)]}'
{"version": 3, "sources": ["/web/static/src/views/view_dialogs/form_view_dialog.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/commands/command_hook.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/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/view.js", "/web/static/src/views/view_hook.js", "/web/static/src/webclient/actions/action_dialog.js", "/web/static/src/webclient/actions/reports/utils.js", "/web/static/src/webclient/actions/reports/report_action.js", "/web/static/src/webclient/actions/reports/report_hook.js", "/web/static/src/views/utils.js", "/web/static/src/views/fields/formatters.js", "/web/static/src/views/fields/file_handler.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/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/channel_member_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_model.js", "/mail/static/src/core/common/country_flag.js", "/mail/static/src/core/common/country_model.js", "/mail/static/src/core/common/date_section.js", "/mail/static/src/core/common/discuss_component_registry.js", "/mail/static/src/core/common/emoji_picker_mobile.js", "/mail/static/src/core/common/failure_model.js", "/mail/static/src/core/common/follower_model.js", "/mail/static/src/core/common/im_status.js", "/mail/static/src/core/common/im_status_service_patch.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_list.js", "/mail/static/src/core/common/link_preview_model.js", "/mail/static/src/core/common/mail_core_common_service.js", "/mail/static/src/core/common/mail_popout_service.js", "/mail/static/src/core/common/message.js", "/mail/static/src/core/common/message_action_menu_mobile.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_model.js", "/mail/static/src/core/common/message_notification_popover.js", "/mail/static/src/core/common/message_reaction_button.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/message_seen_indicator.js", "/mail/static/src/core/common/navigable_list.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/persona_model.js", "/mail/static/src/core/common/picker.js", "/mail/static/src/core/common/picker_content.js", "/mail/static/src/core/common/record.js", "/mail/static/src/core/common/relative_time.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_icon.js", "/mail/static/src/core/common/thread_model.js", "/mail/static/src/core/common/volume_model.js", "/mail/static/src/core/web_portal/message_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/misc.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/discuss/typing/common/typing.js", "/mail/static/src/discuss/core/common/action_panel.js", "/portal/static/src/chatter/core/chatter_patch.js", "/portal/static/src/chatter/core/composer_patch.js", "/portal/static/src/chatter/core/picker_patch.js", "/portal/static/src/chatter/core/thread_model_patch.js", "/portal/static/src/chatter/frontend/attachment_upload_service_patch.js", "/portal/static/src/chatter/frontend/chatter_patch.js", "/portal/static/src/chatter/frontend/composer_model_patch.js", "/portal/static/src/chatter/frontend/composer_patch.js", "/portal/static/src/chatter/frontend/message_patch.js", "/portal/static/src/chatter/frontend/portal_chatter.js", "/portal/static/src/chatter/frontend/portal_chatter_service.js", "/portal/static/src/chatter/frontend/store_service_patch.js", "/portal/static/src/chatter/frontend/thread_model_patch.js", "/rating/static/src/core/common/message_model_patch.js", "/rating/static/src/core/common/rating_model.js", "/portal_rating/static/src/chatter/frontend/composer_patch.js", "/portal_rating/static/src/chatter/frontend/message_patch.js", "/portal_rating/static/src/chatter/frontend/store_service_patch.js", "/portal_rating/static/src/chatter/frontend/thread_model_patch.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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AClLA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvvCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChsBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzgCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACv0BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACz0BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;AClZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1qBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC15EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7ZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;AC7PA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3mBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC9MA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC3BA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;ACtLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACjKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpwBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACplBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpkCA;;;;;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC7HA;;;;;;;;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;;;;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;;;;ACHA;;;;;;;;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;;;;ACXA;;;;;;;;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;AACA;AACA;AACA;AACA;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;;;;ACPA;;;;;;;;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;;;;ACfA;;;;;;;;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;;;;AC3FA;;;;;;;;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;;;;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;;;;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;;;;AChDA;;;;;;;;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", "sourcesContent": ["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, onMounted } 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        mode: {\n            optional: true,\n            validate: (m) => [\"edit\", \"readonly\"].includes(m),\n        },\n        onRecordSaved: { 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        isToMany: { type: Boolean, optional: true },\n        size: Dialog.props.size,\n    };\n    static defaultProps = {\n        onRecordSaved: () => {},\n        preventCreate: false,\n        preventEdit: false,\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        this.viewProps = {\n            type: \"form\",\n            buttonTemplate,\n\n            context: this.props.context || {},\n            display: { controlPanel: false },\n            mode: this.props.mode || \"edit\",\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, { saveAndNew }) => {\n                const saved = await record.save({ reload: false });\n                if (saved) {\n                    this.currentResId = record.resId;\n                    await this.props.onRecordSaved(record);\n                    if (saveAndNew) {\n                        const context = Object.assign({}, this.props.context);\n                        Object.keys(context).forEach((k) => {\n                            if (k.startsWith(\"default_\")) {\n                                delete context[k];\n                            }\n                        });\n                        this.currentResId = false;\n                        await record.model.load({ resId: false, context });\n                    } else {\n                        this.props.close();\n                    }\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        onMounted(() => {\n            if (this.modalRef.el.querySelector(\".modal-footer\").childElementCount > 1) {\n                const defaultButton = this.modalRef.el.querySelector(\n                    \".modal-footer button.o-default-button\"\n                );\n                if (defaultButton) {\n                    defaultButton.classList.add(\"d-none\");\n                }\n            }\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            });\n        }\n    }\n}\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(\"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\nfunction becomeSuperuser({ env }) {\n    const becomeSuperuserURL = browser.location.origin + \"/web/become\";\n    return {\n        type: \"item\",\n        description: _t(\"Become Superuser\"),\n        hide: !user.isAdmin,\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 { 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 { user } from \"@web/core/user\";\nimport { useBus, 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 { EventBus, onWillStart, onWillUpdateProps, useComponent } from \"@odoo/owl\";\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n */\n\nexport class Model {\n    /**\n     * @param {Object} env\n     * @param {Object} services\n     */\n    constructor(env, params, services) {\n        this.env = env;\n        this.orm = services.orm;\n        this.bus = new EventBus();\n        this.setup(params, services);\n    }\n\n    /**\n     * @param {Object} params\n     * @param {Object} services\n     */\n    setup(/* params, services */) {}\n\n    /**\n     * @param {SearchParams} searchParams\n     */\n    async load(/* searchParams */) {}\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}\nModel.services = [];\n\n/**\n * @param {Object} 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        return model.load(component.props);\n    });\n    onWillUpdateProps((nextProps) => model.load(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.onUpdate]\n * @param {Function} [options.onWillStart]\n * @param {Function} [options.onWillStartAfterLoad]\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    const model = new ModelClass(component.env, params, services);\n\n    useBus(\n        model.bus,\n        \"update\",\n        options.onUpdate ||\n            (() => {\n                component.render(true); // FIXME WOWL reactivity\n            })\n    );\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 = useSampleModel;\n    const orm = model.orm;\n    let sampleORM = localState.sampleORM;\n    let started = false;\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        } else {\n            useSampleModel = false;\n            model.useSampleModel = useSampleModel;\n        }\n        if (started) {\n            model.notify();\n        }\n    }\n    onWillStart(async () => {\n        if (options.onWillStart) {\n            await options.onWillStart();\n        }\n        await load(component.props);\n        if (options.onWillStartAfterLoad) {\n            await options.onWillStartAfterLoad();\n        }\n        started = true;\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: () => {\n            return { sampleORM };\n        },\n    });\n\n    return model;\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { 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            return;\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            },\n            hooks: {\n                onRecordSaved: this.props.info.onRecordSaved || (() => {}),\n                onWillSaveRecord: this.props.info.onWillSaveRecord || (() => {}),\n                onRecordChanged: this.props.info.onRecordChanged || (() => {}),\n            },\n        };\n        const modelServices = Object.fromEntries(\n            StandaloneRelationalModel.services.map((servName) => {\n                return [servName, useService(servName)];\n            })\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                    }\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                return this.model.load({ values });\n            } else {\n                return this.model.load();\n            }\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        \"onRecordChanged?\",\n        \"onRecordSaved?\",\n        \"onWillSaveRecord?\",\n    ];\n    setup() {\n        if (this.props.fields) {\n            this.fields = this.props.fields;\n        } else {\n            const orm = useService(\"orm\");\n            onWillStart(async () => {\n                this.fields = await orm.call(\n                    this.props.resModel,\n                    \"fields_get\",\n                    [this.props.fieldNames],\n                    {}\n                );\n            });\n        }\n    }\n}\n", "import { markRaw } from \"@odoo/owl\";\nimport { evalDomain } from \"@web/core/domain\";\nimport { Reactive } from \"@web/core/utils/reactive\";\nimport { getId } from \"./utils\";\n\n/**\n * @typedef Params\n * @property {string} resModel\n * @property {Object} context\n * @property {{[key: string]: FieldInfo}} activeFields\n * @property {{[key: string]: Field}} fields\n */\n\n/**\n * @typedef Field\n * @property {string} name\n * @property {string} type\n * @property {[string,string][]} [selection]\n */\n\n/**\n * @typedef FieldInfo\n * @property {string} context\n * @property {boolean} invisible\n * @property {boolean} readonly\n * @property {boolean} required\n * @property {boolean} onChange\n */\n\nexport class DataPoint extends Reactive {\n    /**\n     * @param {import(\"./relational_model\").RelationalModel} model\n     * @param {import(\"./relational_model\").Config\"} config\n     * @param {any} data\n     * @param {Object} [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        this._config = config;\n        this.setup(config, data, options);\n    }\n\n    /**\n     * @abstract\n     * @param {Object} params\n     * @param {Object} state\n     */\n    setup() {}\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    get currentCompanyId() {\n        return this.config.currentCompanyId;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    /**\n     * @param {string} fieldName\n     * @returns {boolean}\n     */\n    isFieldReadonly(fieldName) {\n        const activeField = this.activeFields[fieldName];\n        const { readonly } = activeField || this.fields[fieldName];\n        return readonly && evalDomain(readonly, this.evalContext);\n    }\n}\n", "//@ts-check\n\nimport { Domain } from \"@web/core/domain\";\nimport { DynamicList } from \"./dynamic_list\";\nimport { getGroupServerValue } from \"./utils\";\n\nexport class DynamicGroupList extends DynamicList {\n    static type = \"DynamicGroupList\";\n\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     * @param {Object} data\n     */\n    setup(config, data) {\n        super.setup(...arguments);\n        this.isGrouped = true;\n        this._nbRecordsMatchingDomain = null;\n        this._setData(data);\n    }\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 {import(\"./record\").Record[]}\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        const value =\n            targetGroup.groupByField.type === \"many2one\"\n                ? [targetGroup.value, targetGroup.displayName]\n                : targetGroup.value;\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        };\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        nextConfigGroups[id] = {\n            ...commonConfig,\n            context,\n            groupByFieldName: this.groupByField.name,\n            isFolded: Boolean(foldField),\n            initialDomain: domain,\n            list: {\n                ...commonConfig,\n                context,\n                domain: domain,\n                groupBy: [],\n                orderBy: this.orderBy,\n            },\n        };\n        this.model._updateConfig(this.config, { groups: nextConfigGroups }, { reload: false });\n\n        const data = {\n            count: 0,\n            length: 0,\n            records: [],\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\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 { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { DataPoint } from \"./datapoint\";\nimport { Record } from \"./record\";\nimport { resequence } from \"./utils\";\n\nconst DEFAULT_HANDLE_FIELD = \"sequence\";\n\nexport class DynamicList extends DataPoint {\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     */\n    setup(config) {\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            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 resIds;\n    }\n\n    async leaveEditMode({ discard } = {}) {\n        if (this.editedRecord) {\n            let canProceed = true;\n            if (discard) {\n                this._recordToDiscard = this.editedRecord;\n                await this.editedRecord.discard();\n                this._recordToDiscard = null;\n                if (this.editedRecord && this.editedRecord.isNew) {\n                    this._removeRecords([this.editedRecord.id]);\n                }\n            } else {\n                if (!this.model._urgentSave) {\n                    await this.editedRecord.checkValidity();\n                    if (!this.editedRecord) {\n                        return true;\n                    }\n                }\n                if (this.editedRecord.isNew && !this.editedRecord.dirty) {\n                    this._removeRecords([this.editedRecord.id]);\n                } else {\n                    canProceed = await this.editedRecord.save();\n                }\n            }\n\n            if (canProceed && this.editedRecord) {\n                this.model._updateConfig(\n                    this.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) {\n        return this.model.mutex.exec(() => this._multiSave(record));\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                orderBy[0] = { name: orderBy[0].name, asc: !orderBy[0].asc };\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    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    async _duplicateRecords(records) {\n        let resIds;\n        if (records.length) {\n            resIds = records.map((r) => r.resId);\n        } else {\n            resIds = await this.getResIds(true);\n        }\n\n        const duplicated = await this.model.orm.call(this.resModel, \"copy\", [resIds], {\n            context: this.context,\n        });\n        if (resIds.length > duplicated.length) {\n            this.model.notification.add(_t(\"Some records could not be duplicated\"), {\n                title: _t(\"Warning\"),\n            });\n        }\n        return this.model.load();\n    }\n\n    async _deleteRecords(records) {\n        let resIds;\n        if (records.length) {\n            resIds = 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, { title: _t(\"Warning\") });\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(record) {\n        const changes = record._getChanges();\n        if (!Object.keys(changes).length || record === this._recordToDiscard) {\n            return;\n        }\n        const validSelection = this.selection.filter((record) => {\n            return Object.keys(changes).every((fieldName) => {\n                if (record._isReadonly(fieldName)) {\n                    return false;\n                } else if (record._isRequired(fieldName) && !changes[fieldName]) {\n                    return false;\n                }\n                return true;\n            });\n        });\n        const canProceed = await this.model.hooks.onWillSaveMulti(record, changes, validSelection);\n        if (canProceed === false) {\n            return false;\n        }\n        if (validSelection.length === 0) {\n            this.model.dialog.add(AlertDialog, {\n                body: _t(\"No valid record to save\"),\n                confirm: () => this.leaveEditMode({ discard: true }),\n                dismiss: () => this.leaveEditMode({ discard: true }),\n            });\n            return false;\n        } else {\n            const resIds = validSelection.map((r) => r.resId);\n            const context = this.context;\n            try {\n                await this.model.orm.write(this.resModel, resIds, changes, { context });\n            } catch (e) {\n                record._discard();\n                this.model._updateConfig(record.config, { mode: \"readonly\" }, { reload: false });\n                throw e;\n            }\n            const records = await this.model._loadRecords({ ...this.config, resIds });\n            for (const record of validSelection) {\n                const serverValues = records.find((r) => r.id === record.resId);\n                record._applyValues(serverValues);\n                this.model._updateSimilarRecords(record, serverValues);\n            }\n            record._discard();\n            this.model._updateConfig(record.config, { mode: \"readonly\" }, { reload: false });\n        }\n        this.model.hooks.onSavedMulti(validSelection);\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        if (resequencedRecords) {\n            for (const dpData of resequencedRecords) {\n                const dp = originalList.find((d) => getResId(d) === dpData.id);\n                if (dp instanceof Record) {\n                    dp._applyValues(dpData);\n                } else {\n                    dp[handleField] = dpData[handleField];\n                }\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, { title: _t(\"Warning\") });\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\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 {import(\"./record\").Record[]} */\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                currentCompanyId: this.currentCompanyId,\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        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.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    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", "import { markRaw, markup, toRaw } from \"@odoo/owl\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\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 { escape } from \"@web/core/utils/strings\";\nimport { DataPoint } from \"./datapoint\";\nimport {\n    createPropertyActiveField,\n    getBasicEvalContext,\n    getFieldContext,\n    getFieldsSpec,\n    parseServerValue,\n} from \"./utils\";\nimport { FetchRecordError } from \"./errors\";\n\nexport class Record extends DataPoint {\n    static type = \"Record\";\n\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     * @param {Object} data\n     * @param {Object} [options={}]\n     * @param {boolean} [options.manuallyAdded]\n     * @param {Function} [options.onUpdate]\n     * @param {Record} [options.parentRecord]\n     * @param {string} [options.virtualId]\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        this._invalidFields = new Set();\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    _setData(data) {\n        this._isEvalContextReady = false;\n        if (this.resId) {\n            this._values = this._parseServerValues(data);\n            this._changes = markRaw({});\n            Object.assign(this._textValues, this._getTextValues(data));\n        } else {\n            this._values = markRaw({});\n            const allVals = { ...this._getDefaultValues(), ...data };\n            this._initialChanges = markRaw(this._parseServerValues(allVals));\n            this._changes = markRaw({ ...this._initialChanges });\n            Object.assign(this._textValues, this._getTextValues(allVals));\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        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    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(this._parseServerValues(this._getDefaultValues()));\n                this._values = markRaw({});\n                this._textValues = markRaw({});\n                this.data = { ...this._changes };\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    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    async save(options) {\n        await this.model._askChanges();\n        return this.model.mutex.exec(() => this._save(options));\n    }\n\n    async setInvalidField(fieldName) {\n        this.dirty = true;\n        return this._setInvalidField(fieldName);\n    }\n\n    async resetFieldValidity(fieldName) {\n        this.dirty = true;\n        return this._resetFieldValidity(fieldName);\n    }\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            const change = changes[fieldName];\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, 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, ...serverChanges }));\n        this._checkValidity({ removeInvalidOnly: true });\n        return undoChanges;\n    }\n\n    _applyDefaultValues() {\n        const fieldNames = this.fieldNames.filter((fieldName) => {\n            return !(fieldName in this.data);\n        });\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._setInvalidField(fieldName);\n            }\n        }\n        const isValid = !this._invalidFields.size;\n        if (!isValid && displayNotification) {\n            const items = [...this._invalidFields].map((fieldName) => {\n                return `<li>${escape(this.fields[fieldName].string || fieldName)}</li>`;\n            }, this);\n            this._closeInvalidFieldsNotification = this.model.notification.add(\n                markup(`<ul>${items.join(\"\")}</ul>`),\n                {\n                    title: _t(\"Invalid fields: \"),\n                    type: \"danger\",\n                }\n            );\n        }\n        return isValid;\n    }\n\n    /**\n     * Given a possibily incomplete value for a many2one field (i.e. a pair [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 {Array | false} value a (possibly incomplete) pair [id, display_name] or false\n     * @param {string} fieldName\n     * @param {string} resModel\n     * @returns the completed pair [id, display_name] or false\n     */\n    async _completeMany2OneValue(value, fieldName, resModel) {\n        const resId = value[0];\n        const displayName = value[1];\n        if (!resId && !displayName) {\n            return false;\n        }\n        const context = getFieldContext(this, fieldName);\n        if (!resId && displayName !== undefined) {\n            return this.model.orm.call(resModel, \"name_create\", [displayName], { context });\n        }\n        if (resId && displayName === undefined) {\n            const kwargs = {\n                context,\n                specification: { display_name: {} },\n            };\n            const records = await this.model.orm.webRead(resModel, [resId], kwargs);\n            return [resId, records[0].display_name];\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[0];\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    _createStaticListDatapoint(data, fieldName) {\n        const { related, limit, defaultOrderBy } = this.activeFields[fieldName];\n        const config = {\n            resModel: this.fields[fieldName].relation,\n            activeFields: (related && related.activeFields) || {},\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: defaultOrderBy || [],\n            limit: limit || Number.MAX_SAFE_INTEGER,\n            currentCompanyId: this.currentCompanyId,\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(this.isNew ? { ...this._initialChanges } : {});\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        this._closeInvalidFieldsNotification();\n        this._closeInvalidFieldsNotification = () => {};\n        this._restoreActiveFields();\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[0] : 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                let value;\n                if (property.type === \"many2one\") {\n                    value = property.value;\n                } else if (\n                    (property.type === \"date\" || property.type === \"datetime\") &&\n                    typeof property.value === \"string\"\n                ) {\n                    // TO REMOVE: need refactoring PropertyField to use the same format as the server\n                    value = property.value;\n                } else {\n                    value = this._formatServerValue(property.type, property.value);\n                }\n                return {\n                    ...property,\n                    value,\n                };\n            });\n        }\n        return value;\n    }\n\n    _getChanges(changes = this._changes, { withReadonly } = {}) {\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    _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    _isInvisible(fieldName) {\n        const invisible = this.activeFields[fieldName].invisible;\n        return invisible ? evaluateBooleanExpr(invisible, this.evalContextWithVirtualIds) : false;\n    }\n\n    _isReadonly(fieldName) {\n        const readonly = this.activeFields[fieldName].readonly;\n        return readonly ? evaluateBooleanExpr(readonly, this.evalContextWithVirtualIds) : false;\n    }\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     * @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 relatedPropertyField = {\n            fieldName,\n        };\n        if (parent) {\n            relatedPropertyField.id = parent[0];\n            relatedPropertyField.displayName = parent[1];\n        }\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                    propertyName: property.name,\n                    relation: property.comodel,\n                };\n            }\n            if (hasCurrentValues || !this.activeFields[propertyFieldName]) {\n                this.activeFields[propertyFieldName] = createPropertyActiveField(property);\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.length && property.value[1] === null\n                        ? [property.value[0], _t(\"No Access\")]\n                        : property.value;\n            } else {\n                data[propertyFieldName] = property.value ?? false;\n            }\n        }\n\n        return data;\n    }\n\n    _parseServerValues(serverValues, currentValues = {}) {\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) => {\n                            return { id: resId };\n                        });\n                    }\n                    staticList = this._createStaticListDatapoint(data, fieldName);\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                        [value.resId, value.displayName],\n                        fieldName,\n                        relation\n                    ).then((v) => {\n                        changes[fieldName] = { resId: v[0], displayName: v[1] };\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                        [value.resId, value.displayName],\n                        fieldName,\n                        value.resModel\n                    ).then((v) => {\n                        changes[fieldName] = {\n                            resId: v[0],\n                            resModel: value.resModel,\n                            displayName: v[1],\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    _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        const changes = this._getChanges();\n        delete changes.id; // id never changes, and should not be written\n        if (!creation && !Object.keys(changes).length) {\n            return true;\n        }\n        if (!this._checkValidity({ displayNotification: true })) {\n            return false;\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.model._closeUrgentSaveNotification = this.model.notification.add(\n                    markup(\n                        _t(\n                            `Heads up! Your recent changes are too large to save automatically. Please click the <i class=\"fa fa-cloud-upload fa-fw\"></i> button now to ensure your work is saved before you exit this tab.`\n                        )\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        let fieldSpec = {};\n        if (reload) {\n            fieldSpec = getFieldsSpec(\n                this.activeFields,\n                this.fields,\n                getBasicEvalContext(this.config)\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, { discard: () => this._discard() });\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]);\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    async _setInvalidField(fieldName) {\n        const canProceed = this.model.hooks.onWillSetInvalidField(this, fieldName);\n        if (canProceed === false) {\n            return;\n        }\n        if (\n            this.selected &&\n            this.model.multiEdit &&\n            this.model.root._recordToDiscard !== this &&\n            !this._invalidFields.has(fieldName)\n        ) {\n            await this.model.dialog.add(AlertDialog, {\n                body: _t(\"No valid record to save\"),\n                confirm: async () => {\n                    await this.discard();\n                    this.switchMode(\"readonly\");\n                },\n            });\n        }\n        this._invalidFields.add(fieldName);\n    }\n\n    _resetFieldValidity(fieldName) {\n        this._invalidFields.delete(fieldName);\n    }\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        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            this._applyChanges(changes);\n            return this.model.root._multiSave(this);\n        }\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 (curVal && nextVal && curVal[0] === nextVal[0] && curVal[1] === nextVal[1]) {\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 { shallowEqual } from \"@web/core/utils/arrays\";\nimport { 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 } from \"./record\";\nimport { StaticList } from \"./static_list\";\nimport {\n    extractInfoFromGroupData,\n    getBasicEvalContext,\n    getFieldsSpec,\n    isRelational,\n    makeActiveField,\n} from \"./utils\";\nimport { FetchRecordError } from \"./errors\";\n\n/**\n * @typedef Params\n * @property {Config} config\n * @property {State} [state]\n * @property {Hooks} [hooks]\n * @property {number} [limit]\n * @property {number} [countLimit]\n * @property {number} [groupsLimit]\n * @property {string[]} [defaultOrderBy]\n * @property {string[]} [defaultGroupBy]\n * @property {number} [maxGroupByDepth]\n * @property {boolean} [multiEdit]\n * @property {Object} [groupByInfo]\n * @property {number} [activeIdsLimit]\n * @property {boolean} [useSendBeaconToSaveUrgently]\n */\n\n/**\n * @typedef Config\n * @property {string} resModel\n * @property {Object} fields\n * @property {Object} activeFields\n * @property {object} context\n * @property {boolean} isMonoRecord\n * @property {number} currentCompanyId\n * @property {boolean} isRoot\n * @property {Array} [domain]\n * @property {Array} [groupBy]\n * @property {Array} [orderBy]\n * @property {number} [resId]\n * @property {number[]} [resIds]\n * @property {string} [mode]\n * @property {number} [limit]\n * @property {number} [offset]\n * @property {number} [countLimit]\n * @property {number} [groupsLimit]\n * @property {Object} [groups]\n * @property {Object} [currentGroups] // FIXME: could be cleaned\n * @property {boolean} [openGroupsByDefault]\n */\n\n/**\n * @typedef Hooks\n * @property {(nextConfiguration: Config) => void} [onWillLoadRoot]\n * @property {() => Promise} [onRootLoaded]\n * @property {Function} [onWillSaveRecord]\n * @property {Function} [onRecordSaved]\n * @property {Function} [onWillSaveMulti]\n * @property {Function} [onSavedMulti]\n * @property {Function} [onWillSetInvalidField]\n * @property {Function} [onRecordChanged]\n */\n\n/**\n * @typedef State\n * @property {Config} config\n * @property {Object} specialDataCaches\n */\n\nconst DEFAULT_HOOKS = {\n    onWillLoadRoot: () => {},\n    onRootLoaded: () => {},\n    onWillSaveRecord: () => {},\n    onRecordSaved: () => {},\n    onWillSaveMulti: () => {},\n    onSavedMulti: () => {},\n    onWillSetInvalidField: () => {},\n    onRecordChanged: () => {},\n};\n\nexport class RelationalModel extends Model {\n    static services = [\"action\", \"company\", \"dialog\", \"notification\", \"orm\"];\n    static Record = Record;\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;\n    static MAX_NUMBER_OPENED_GROUPS = 10;\n\n    /**\n     * @param {Params} params\n     */\n    setup(params, { action, company, 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 {Config} */\n        this.config = {\n            isMonoRecord: false,\n            currentCompanyId: company.currentCompany.id,\n            context: {},\n            ...params.config,\n            isRoot: true,\n        };\n\n        /** @type {Hooks} */\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.defaultGroupBy = params.defaultGroupBy;\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\n        this._urgentSave = false;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    exportState() {\n        return {\n            config: toRaw(this.config),\n            specialDataCaches: this.specialDataCaches,\n        };\n    }\n\n    hasData() {\n        return this.root.hasData;\n    }\n\n    /**\n     * @param {Object} [params={}]\n     * @param {Comparison | null} [params.comparison]\n     * @param {Context} [params.context]\n     * @param {DomainListRepr} [params.domain]\n     * @param {string[]} [params.groupBy]\n     * @param {Object[]} [params.orderBy]\n     * @returns {Promise<void>}\n     */\n    async load(params = {}) {\n        const config = this._getNextConfig(this.config, params);\n        this.hooks.onWillLoadRoot(config);\n        const data = await this.keepLast.add(this._loadData(config));\n        this.root = this._createRoot(config, data);\n        this.config = config;\n        return this.hooks.onRootLoaded();\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    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    _askChanges() {\n        const proms = [];\n        this.bus.trigger(\"NEED_LOCAL_CHANGES\", { proms });\n        return Promise.all([...proms, this.mutex.getUnlockedDef()]);\n    }\n\n    /**\n     *\n     * @param {Config} config\n     * @param {*} 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    /**\n     * @param {*} params\n     * @returns {Config}\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        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            config.comparison = \"comparison\" in params ? params.comparison : config.comparison;\n\n            // groupBy\n            config.groupBy = \"groupBy\" in params ? params.groupBy : config.groupBy;\n            // apply default groupBy if any\n            if (this.defaultGroupBy && !config.groupBy.length) {\n                config.groupBy = [this.defaultGroupBy];\n            }\n            // restrict the number of groupbys if requested\n            if (this.maxGroupByDepth) {\n                config.groupBy = config.groupBy.slice(0, this.maxGroupByDepth);\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 && this.root) {\n            // always reset the offset to 0 when reloading from above\n            const resetOffset = (config) => {\n                config.offset = 0;\n                for (const group of Object.values(config.groups || {})) {\n                    resetOffset(group.list);\n                }\n            };\n            resetOffset(config);\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 {Config} config\n     */\n    async _loadData(config) {\n        if (config.isMonoRecord) {\n            const evalContext = getBasicEvalContext(config);\n            if (!config.resId) {\n                return this._loadNewRecord(config, { evalContext });\n            }\n            const records = await this._loadRecords(\n                {\n                    ...config,\n                    resIds: [config.resId],\n                },\n                evalContext\n            );\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);\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        return this._loadUngroupedList({\n            ...config,\n            context: {\n                ...config.context,\n                current_company_id: config.currentCompanyId,\n            },\n        });\n    }\n\n    /**\n     * @param {Config} config\n     */\n    async _loadGroupedList(config) {\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        const firstGroupByName = config.groupBy[0].split(\":\")[0];\n        if (firstGroupByName.includes(\".\")) {\n            if (!config.fields[firstGroupByName]) {\n                await this._getPropertyDefinition(config, firstGroupByName);\n            }\n            const propertiesFieldName = firstGroupByName.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 orderBy = config.orderBy.filter(\n            (o) =>\n                o.name === firstGroupByName ||\n                o.name === \"__count\" ||\n                (o.name in config.activeFields && config.fields[o.name].aggregator !== undefined)\n        );\n        const response = await this._webReadGroup(config, orderBy);\n        const { groups: groupsData, length } = response;\n        const groupBy = config.groupBy.slice(1);\n        const groupByField = config.fields[config.groupBy[0].split(\":\")[0]];\n        const commonConfig = {\n            resModel: config.resModel,\n            fields: config.fields,\n            activeFields: config.activeFields,\n        };\n        let groupRecordConfig;\n        const groupRecordResIds = [];\n        if (this.groupByInfo[firstGroupByName]) {\n            groupRecordConfig = {\n                ...this.groupByInfo[firstGroupByName],\n                resModel: config.fields[firstGroupByName].relation,\n                context: {},\n            };\n        }\n        const proms = [];\n        let nbOpenGroups = 0;\n\n        const groups = [];\n        for (const groupData of groupsData) {\n            const group = extractInfoFromGroupData(groupData, config.groupBy, config.fields);\n            if (!config.groups[group.value]) {\n                config.groups[group.value] = {\n                    ...commonConfig,\n                    groupByFieldName: groupByField.name,\n                    isFolded:\n                        \"__fold\" in groupData ? groupData.__fold : !config.openGroupsByDefault,\n                    extraDomain: false,\n                    value: group.value,\n                    list: {\n                        ...commonConfig,\n                        groupBy,\n                    },\n                };\n                if (isRelational(config.fields[firstGroupByName]) && !group.value) {\n                    // fold the \"unset\" group by default when grouped by many2one\n                    config.groups[group.value].isFolded = true;\n                }\n                if (groupRecordConfig) {\n                    config.groups[group.value].record = {\n                        ...groupRecordConfig,\n                        resId: group.value ?? false,\n                    };\n                }\n            }\n            if (groupRecordConfig) {\n                const resId = config.groups[group.value].record.resId;\n                if (resId) {\n                    groupRecordResIds.push(resId);\n                }\n            }\n            const groupConfig = config.groups[group.value];\n            groupConfig.list.orderBy = config.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                ...config.context,\n                [`default_${firstGroupByName}`]: group.serverValue,\n            };\n            groupConfig.list.context = context;\n            groupConfig.context = context;\n            if (groupBy.length) {\n                group.groups = [];\n            } else {\n                group.records = [];\n            }\n            if (!groupConfig.isFolded) {\n                nbOpenGroups++;\n                if (nbOpenGroups > this.constructor.MAX_NUMBER_OPENED_GROUPS) {\n                    groupConfig.isFolded = true;\n                }\n            }\n            if (!groupConfig.isFolded && group.count > 0) {\n                const prom = this._loadData(groupConfig.list).then((response) => {\n                    if (groupBy.length) {\n                        group.groups = response ? response.groups : [];\n                        group.length = response ? response.length : 0;\n                    } else {\n                        group.records = response ? response.records : [];\n                    }\n                });\n                proms.push(prom);\n            }\n            groups.push(group);\n        }\n        if (groupRecordConfig && Object.keys(groupRecordConfig.activeFields).length) {\n            const prom = this._loadRecords({\n                ...groupRecordConfig,\n                resIds: groupRecordResIds,\n            }).then((records) => {\n                for (const group of groups) {\n                    if (!group.value) {\n                        group.values = { id: false };\n                        continue;\n                    }\n                    group.values = records.find((r) => group.value && r.id === group.value);\n                }\n            });\n            proms.push(prom);\n        }\n        await Promise.all(proms);\n\n        // if a group becomes empty at some point (e.g. we dragged its last record out of it), and the view is reloaded\n        // with the same domain and groupbys, we want to keep the empty group in the UI\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                        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 {Config} config\n     * @param {Object} [params={}]\n     * @returns Promise<Object>\n     */\n    async _loadNewRecord(config, params = {}) {\n        return this._onchange(config, params);\n    }\n\n    /**\n     *\n     * @param {Config} config\n     * @param {object} evalContext\n     * @returns\n     */\n    async _loadRecords(config, evalContext = config.context) {\n        const { resModel, resIds, activeFields, fields, context } = config;\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 records = await this.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) => {\n                return { id: resId };\n            });\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 {Config} config\n     * @returns\n     */\n    async _loadUngroupedList(config) {\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        return this.orm.webSearchRead(config.resModel, config.domain, kwargs);\n    }\n\n    /**\n     * @param {Config} config\n     * @param {Object} param\n     * @param {Object} [param.changes={}]\n     * @param {string[]} [param.fieldNames=[]]\n     * @param {Object} [param.evalContext=config.context]\n     * @returns Promise<Object>\n     */\n    async _onchange(\n        config,\n        { changes = {}, fieldNames = [], evalContext = config.context, onError }\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            response = await this.orm.call(resModel, \"onchange\", args, { context });\n        } catch (e) {\n            if (onError) {\n                return onError(e);\n            }\n            throw e;\n        }\n        if (response.warning) {\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        return response.value;\n    }\n\n    /**\n     *\n     * @param {Config} config\n     * @param {Partial<Config>} patch\n     * @param {Object} [options]\n     * @param {boolean} [options.reload=true]\n     * @param {Function} [options.commit] Function to call once the data has been loaded\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            return this.hooks.onRootLoaded();\n        }\n    }\n\n    /**\n     *\n     * @param {Config} config\n     * @returns {Promise<number>}\n     */\n    async _updateCount(config) {\n        const count = await this.keepLast.add(this.orm.searchCount(config.resModel, config.domain));\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    _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    async _webReadGroup(config, orderBy) {\n        const aggregates = Object.values(config.fields)\n            .filter((field) => field.aggregator && field.name in config.activeFields)\n            .map((field) => `${field.name}:${field.aggregator}`);\n        return this.orm.webReadGroup(\n            config.resModel,\n            config.domain,\n            aggregates,\n            [config.groupBy[0]],\n            {\n                orderby: orderByToString(orderBy),\n                lazy: true,\n                offset: config.offset,\n                limit: config.limit, // TODO: remove limit when == MAX_integer\n                context: config.context,\n            }\n        );\n    }\n}\n", "import { x2ManyCommands } from \"@web/core/orm_service\";\nimport { intersection } from \"@web/core/utils/arrays\";\nimport { 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\nfunction compareFieldValues(v1, v2, fieldType) {\n    if (fieldType === \"many2one\") {\n        v1 = v1 ? v1[1] : \"\";\n        v2 = v2 ? v2[1] : \"\";\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\nexport class StaticList extends DataPoint {\n    static type = \"StaticList\";\n\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     * @param {Object} data\n     * @param {Object} [options={}]\n     * @param {Function} [options.onUpdate]\n     * @param {Record} [options.parent]\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        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    // -------------------------------------------------------------------------\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    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            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 {Record} [record]\n     * @returns {Record}\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            if (this.editedRecord) {\n                const isValid = this.editedRecord._checkValidity();\n                if (!isValid && validate) {\n                    return false;\n                }\n                if (canAbandon !== false && !validate) {\n                    this._abandonRecords([this.editedRecord], { force: true });\n                }\n                // if we still have an editedRecord, it means it hasn't been abandonned\n                if (this.editedRecord) {\n                    if (isValid && !this.editedRecord.dirty && discard) {\n                        return false;\n                    }\n                    if (\n                        isValid ||\n                        (!this.editedRecord.dirty && !this.editedRecord._manuallyAdded)\n                    ) {\n                        this.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            if (this.editedRecord && !(await this.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, reload } = {}) {\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, reload });\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 {Record} 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            if (this.orderBy.length) {\n                await this._sort();\n            }\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 } = {}) {\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) {\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    _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, reload } = {}) {\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(record._parseServerValues(changes, record.data));\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.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 || reload) {\n            const resIds = reload\n                ? this.records.map((r) => r.resId)\n                : recordsToLoad.map((r) => r.resId);\n            return this.model._loadRecords({ ...this.config, resIds }).then((recordValues) => {\n                if (reload) {\n                    for (const record of recordValues) {\n                        this._createRecordDatapoint(record);\n                    }\n                    this.records = resIds.map((id) => this._cache[id]);\n                    return;\n                }\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            currentCompanyId: this.currentCompanyId,\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    _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            return 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                    orderBy[0] = { name: orderBy[0].name, asc: !orderBy[0].asc };\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 { rpc } from \"@web/core/network/rpc\";\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\nconst 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        }\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 (fields[fieldName].type === \"many2one_reference\" && fieldNode.views) {\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: config.currentCompanyId,\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, { 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                    if (defaultOrderBy) {\n                        fieldsSpec[fieldName].order = orderByToString(defaultOrderBy);\n                    }\n                }\n                break;\n            }\n            case \"many2one\":\n            case \"reference\": {\n                fieldsSpec[fieldName].fields = {};\n                if (!isAlwaysInvisible) {\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                return value;\n            }\n            return value ? [value.id, value.display_name] : false;\n        }\n        case \"properties\": {\n            return value\n                ? value.map((property) => ({\n                      ...property,\n                      value: parseServerValue(property, property.value ?? false),\n                  }))\n                : [];\n        }\n    }\n    return value;\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) {\n    const info = {};\n    const groupByField = fields[groupBy[0].split(\":\")[0]];\n    // sometimes the key FIELD_ID_count doesn't exist and we have to get the count from `__count` instead\n    // see read_group in models.py\n    info.count = groupData.__count || groupData[`${groupByField.name}_count`];\n    info.length = info.count; // TODO: remove\n    info.range = groupData.__range ? groupData.__range[groupBy[0]] : null;\n    info.domain = groupData.__domain;\n    info.rawValue = groupData[groupBy[0]];\n    info.value = getValueFromGroupData(groupByField, info.rawValue, info.range);\n    info.displayName = getDisplayNameFromGroupData(groupByField, info.rawValue);\n    info.serverValue = getGroupServerValue(groupByField, info.value);\n    info.aggregates = getAggregatesFromGroupData(groupData, fields);\n    return info;\n}\n\n/**\n * @param {Object} groupData\n * @returns {Object}\n */\nfunction getAggregatesFromGroupData(groupData, fields) {\n    const aggregates = {};\n    for (const [key, value] of Object.entries(groupData)) {\n        if (key in fields && AGGREGATABLE_FIELD_TYPES.includes(fields[key].type)) {\n            aggregates[key] = value;\n        }\n    }\n    return aggregates;\n}\n\n/**\n * @param {import(\"./datapoint\").Field} field\n * @param {any} rawValue\n * @returns {string | false}\n */\nfunction getDisplayNameFromGroupData(field, rawValue) {\n    if (field.type === \"selection\") {\n        return Object.fromEntries(field.selection)[rawValue];\n    }\n    if ([\"many2one\", \"many2many\", \"tags\"].includes(field.type)) {\n        return rawValue ? rawValue[1] : false;\n    }\n    return rawValue;\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, range) {\n    if ([\"date\", \"datetime\"].includes(field.type)) {\n        if (!range) {\n            return false;\n        }\n        const dateValue = parseServerValue(field, range.to);\n        return dateValue.minus({\n            [field.type === \"date\" ? \"day\" : \"second\"]: 1,\n        });\n    }\n    const value = parseServerValue(field, rawValue);\n    if ([\"many2one\", \"many2many\"].includes(field.type)) {\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 alive = true;\n    let props = component.props;\n    const fct = () => {\n        const def = new Deferred();\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 (!alive) {\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        alive = false;\n    });\n    onWillStart(() => fct());\n    onWillUpdateProps((nextProps) => {\n        const currentRecordId = props.record.id;\n        props = nextProps;\n        if (props.record.id !== currentRecordId) {\n            return fct();\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 = sequences.length && Math.min(...sequences);\n\n    // Try to write new sequences on the affected records/groups\n    const params = {\n        model: resModel,\n        ids: resIds,\n        context: context,\n        field: fieldName,\n    };\n    if (offset) {\n        params.offset = offset;\n    }\n    try {\n        const wasResequenced = await rpc(\"/web/dataset/resequence\", params);\n        if (!wasResequenced) {\n            return;\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    // Read the actual values set by the server and update the records/groups\n    const kwargs = { context };\n    return orm.read(resModel, resIds, [fieldName], kwargs);\n}\n", "import {\n    deserializeDate,\n    deserializeDateTime,\n    parseDate,\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 } from \"@web/core/utils/arrays\";\nimport { parseServerValue } from \"./relational_model/utils\";\n\nclass UnimplementedRouteError extends Error {}\n\nlet searchReadNumber = 0;\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    let dateValue = parseServerValue(field, range.to);\n    dateValue = dateValue.minus({\n        [field.type === \"date\" ? \"day\" : \"second\"]: 1,\n    });\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 = /(?<measure>\\w+):(?<aggregateFunction>\\w+)(\\((?<fieldName>\\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\");\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 \"read_group\":\n                return this._mockReadGroup(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 values = {};\n        for (const { fieldName, type, aggregateFunction } of measures) {\n            if ([\"float\", \"integer\", \"monetary\"].includes(type)) {\n                if (aggregateFunction === \"array_agg\") {\n                    values[fieldName] = (records || []).map((r) => r[fieldName]);\n                } else if (records.length) {\n                    let value = 0;\n                    for (const record of records) {\n                        value += record[fieldName];\n                    }\n                    values[fieldName] = this._sanitizeNumber(value);\n                } else {\n                    values[fieldName] = null;\n                }\n            }\n            if (type === \"many2one\") {\n                const ids = new Set(records.map((r) => r[fieldName]));\n                values.fieldName = ids.size || null;\n            }\n        }\n        return values;\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)) {\n            const fmt = SampleServer.FORMATS[interval];\n            return parseDate(value).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 read_group method.\n     *\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {string[]} [params.fields] defaults to the list of all fields\n     * @param {string[]} params.groupBy\n     * @param {boolean} [params.lazy=true]\n     * @returns {Object[]} Object with keys groups and length\n     */\n    _mockReadGroup(params) {\n        const lazy = \"lazy\" in params ? params.lazy : true;\n        const model = params.model;\n        const fields = this.data[model].fields;\n        const records = this.data[model].records;\n\n        const normalizedGroupBys = [];\n        let groupBy = [];\n        if (params.groupBy.length) {\n            groupBy = lazy ? [params.groupBy[0]] : params.groupBy;\n        }\n        for (const groupBySpec of groupBy) {\n            let [fieldName, interval] = groupBySpec.split(\":\");\n            interval = interval || \"month\";\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 } = 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) => ({ [fieldName]: 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 measures = [];\n        for (const measureSpec of params.fields || Object.keys(fields)) {\n            const matches = measureSpec.match(MEASURE_SPEC_REGEX);\n            let { fieldName, aggregateFunction, measure } = (matches && matches.groups) || {};\n            if (!aggregateFunction && fieldName in fields && fields[fieldName].aggregator) {\n                aggregateFunction = fields[fieldName].aggregator;\n                measure = fieldName;\n            }\n            if (!fieldName && !measure) {\n                continue; // this is for _count measure\n            }\n            const fName = fieldName || measure;\n            const { type } = fields[fName];\n            if (\n                !params.groupBy.includes(fName) &&\n                type &&\n                (type !== \"many2one\" || aggregateFunction !== \"count_distinct\")\n            ) {\n                measures.push({ fieldName: fName, type, aggregateFunction });\n            }\n        }\n\n        let result = [];\n        for (const id in groups) {\n            const records = groups[id];\n            const group = { __domain: [] };\n            let countKey = `__count`;\n            if (normalizedGroupBys.length && lazy) {\n                countKey = `${normalizedGroupBys[0].fieldName}_count`;\n            }\n            group[countKey] = records.length;\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                    if ([\"date\", \"datetime\"].includes(type)) {\n                        group.__range = {};\n                        const val = firstElem[fieldName];\n                        if (val) {\n                            const deserialize =\n                                type === \"date\" ? deserializeDate : deserializeDateTime;\n                            const serialize = type === \"date\" ? serializeDate : serializeDateTime;\n                            const from = deserialize(val).startOf(gb.interval);\n                            const to = SampleServer.INTERVALS[gb.interval](from);\n                            group.__range[alias] = { from: serialize(from), to: serialize(to) };\n                        } else {\n                            group.__range[alias] = false;\n                        }\n                    }\n                }\n            }\n            Object.assign(group, this._aggregateFields(measures, records));\n            result.push(group);\n        }\n        if (normalizedGroupBys.length > 0) {\n            const { alias, interval, type } = normalizedGroupBys[0];\n            result = arraySortBy(result, (group) => {\n                const val = group[alias];\n                if ([\"date\", \"datetime\"].includes(type)) {\n                    return parseDate(val, { format: SampleServer.FORMATS[interval] });\n                }\n                return val;\n            });\n        }\n        return result;\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.split(\":\")[0];\n        const progress_bar = params.progress_bar;\n        const groupByField = this.data[params.model].fields[groupBy];\n        const data = {};\n        for (const record of this.data[params.model].records) {\n            let groupByValue = record[groupBy];\n            if (groupByField.type === \"many2one\") {\n                const relatedRecords = this.data[groupByField.relation].records;\n                const relatedRecord = relatedRecords.find((r) => r.id === groupByValue);\n                groupByValue = relatedRecord.display_name;\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            if (!(groupByValue in data)) {\n                data[groupByValue] = {};\n                for (const key in progress_bar.colors) {\n                    data[groupByValue][key] = 0;\n                }\n            }\n            const fieldValue = record[progress_bar.field];\n            if (fieldValue in data[groupByValue]) {\n                data[groupByValue][fieldValue]++;\n            }\n        }\n        return data;\n    }\n\n    _mockWebSearchReadUnity(params) {\n        const fields = Object.keys(params.specification);\n        let result;\n        if (this.existingGroups) {\n            const groups = this.existingGroups;\n            const group = groups[searchReadNumber++ % groups.length];\n            result = {\n                records: this._mockRead({\n                    model: params.model,\n                    args: [group.__recordIds, fields],\n                }),\n                length: group.__recordIds.length,\n            };\n        } else {\n            const model = this.data[params.model];\n            const rawRecords = model.records.slice(0, SampleServer.SEARCH_READ_LIMIT);\n            const records = this._mockRead({\n                model: params.model,\n                args: [rawRecords.map((r) => r.id), fields],\n            });\n            result = { records, length: records.length };\n        }\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        let groups;\n        if (this.existingGroups) {\n            this._tweakExistingGroups(params);\n            groups = this.existingGroups;\n        } else {\n            groups = this._mockReadGroup(params);\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                return { id: g[groupBy][0], 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(\n                    group.__range[params.groupBy[0]],\n                    groupByField\n                );\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.fields\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        const fields = params.fields.map((aggregate_spec) => aggregate_spec.split(\":\")[0])\n        for (const g of groups) {\n            const recordsInGroup = records.filter((r) => {\n                if ([\"date\", \"datetime\"].includes(groupByField.type)) {\n                    return (\n                        r[groupBy] === serializeGroupDateValue(g.__range[fullGroupBy], groupByField)\n                    );\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 field of fields) {\n                const fieldType = this.data[params.model].fields[field].type;\n                if ([\"integer, float\", \"monetary\"].includes(fieldType)) {\n                    g[field] = recordsInGroup.reduce((acc, r) => acc + r[field], 0);\n                }\n            }\n            g[`${groupBy}_count`] = recordsInGroup.length;\n            g.__recordIds = recordsInGroup.map((r) => r.id);\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.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    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(() => {\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        }\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", "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\";\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", "import { registry } from \"@web/core/registry\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { ActionMenus } from \"@web/search/action_menus/action_menus\";\n\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    };\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.actionItems, ...this.registryItems].sort((item1, item2) => {\n            const grp = (item1.groupNumber || 0) - (item2.groupNumber || 0);\n            if (grp !== 0) {\n                return grp;\n            }\n            return (item1.sequence || 0) - (item2.sequence || 0);\n        });\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, useExternalListener, 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\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.isEmbeddedActionsOrderModifiable = false;\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        /**\n         * The visible embedded actions are unique to each user and to each res_id. The visible actions chosen by the\n         * user are stored in the local storage in a key corresponding to a combination of the actionId, the activeId\n         * and the currrent userId. Each key contains a dict. The keys of the latter are the id of the visible embedded\n         * actions.\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        this.embeddedActionsVisibilityKey = `visibleEmbeddedActions${parentActionId}+${\n            this.env.searchModel?.globalContext.active_id || \"\"\n        }+${user.userId}`;\n\n        this.embeddedVisibilityKey = `visibleEmbedded${parentActionId}+${\n            this.env.searchModel?.globalContext.active_id || \"\"\n        }+${user.userId}`;\n\n        this.embeddedOrderKey = `orderEmbedded${parentActionId}+${\n            this.env.searchModel?.globalContext.active_id || \"\"\n        }+${user.userId}`;\n\n        this.state = useState({\n            showSearchBar: false,\n            showMobileSearch: false,\n            showViewSwitcher: false,\n            embeddedInfos: {\n                showEmbedded:\n                    this.env.config.embeddedActions?.length > 0 &&\n                    ((!!this.env.config.parentActionId &&\n                        !!JSON.parse(browser.localStorage.getItem(\"showEmbeddedActions\"))) ||\n                        !!JSON.parse(browser.localStorage.getItem(this.embeddedVisibilityKey))),\n                embeddedActions: this.defaultEmbeddedActions || [],\n                newActionIsShared: false,\n                newActionName: this.newActionNameGetter,\n                visibleEmbeddedActions:\n                    (this.env.config.embeddedActions?.length > 0 &&\n                        JSON.parse(\n                            browser.localStorage.getItem(this.embeddedActionsVisibilityKey)\n                        )) ||\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        useExternalListener(window, \"click\", this.onWindowClick);\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            scrollingEl.addEventListener(\"scroll\", this.onScrollThrottledBound);\n            this.root.el.style.top = \"0px\";\n            return () => {\n                scrollingEl.removeEventListener(\"scroll\", this.onScrollThrottledBound);\n            };\n        });\n\n        onMounted(async () => {\n            if (this.state.embeddedInfos.embeddedActions?.length > 0) {\n                // If there is no visible embedded actions, the current action (if it exists) is put by default\n                const embeddedActionKey =\n                    this.state.embeddedInfos.currentEmbeddedAction?.id || false;\n                if (\n                    !Object.keys(this.state.embeddedInfos.visibleEmbeddedActions).includes(\n                        embeddedActionKey.toString()\n                    )\n                ) {\n                    this._setVisibility(embeddedActionKey);\n                }\n                const embeddedOrderLocalStorageKey = browser.localStorage.getItem(\n                    this.embeddedOrderKey\n                );\n                if (embeddedOrderLocalStorageKey) {\n                    this._sortEmbeddedActions(JSON.parse(embeddedOrderLocalStorageKey));\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        this.mainButtons = useRef(\"mainButtons\");\n\n        useEffect(() => {\n            // on small screen, clean-up the dropdown elements\n            const dropdownButtons = this.mainButtons.el.querySelectorAll(\n                \".o_control_panel_collapsed_create.dropdown-menu button\"\n            );\n            if (!dropdownButtons.length) {\n                this.mainButtons.el\n                    .querySelectorAll(\n                        \".o_control_panel_collapsed_create.dropdown-menu, .o_control_panel_collapsed_create.dropdown-toggle\"\n                    )\n                    .forEach((el) => el.classList.add(\"d-none\"));\n                this.mainButtons.el\n                    .querySelectorAll(\".o_control_panel_collapsed_create.btn-group\")\n                    .forEach((el) => el.classList.remove(\"btn-group\"));\n                return;\n            }\n            for (const button of dropdownButtons) {\n                for (const cl of Array.from(button.classList)) {\n                    button.classList.toggle(cl, !cl.startsWith(\"btn-\"));\n                }\n                button.classList.add(\"dropdown-item\", \"btn\", \"btn-link\");\n            }\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    getDropdownClass(action) {\n        return (!this.env.isSmall && this._checkValueLocalStorage(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     * Reset mobile search state\n     */\n    resetSearchState() {\n        Object.assign(this.state, {\n            showSearchBar: false,\n            showMobileSearch: false,\n            showViewSwitcher: false,\n        });\n    }\n\n    /**\n     * @returns {Object}\n     */\n    get display() {\n        return {\n            layoutActions: true,\n            ...this.props.display,\n        };\n    }\n\n    onClickShowEmbedded() {\n        if (this.state.embeddedInfos.showEmbedded) {\n            browser.localStorage.removeItem(this.embeddedVisibilityKey);\n        } else {\n            browser.localStorage.setItem(this.embeddedVisibilityKey, true);\n        }\n        this.state.embeddedInfos.showEmbedded = !this.state.embeddedInfos.showEmbedded;\n        browser.localStorage.setItem(\"showEmbeddedActions\", 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\n                this.lastScrollTop = Math.min(0, this.lastScrollTop - delta);\n            } else {\n                // Going down | not moving\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 {ViewType} viewType\n     */\n    switchView(viewType) {\n        this.resetSearchState();\n        this.actionService.switchView(viewType);\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     * @private\n     * @param {MouseEvent} ev\n     */\n    onWindowClick(ev) {\n        if (this.state.showViewSwitcher && !ev.target.closest(\".o_cp_switch_buttons\")) {\n            this.state.showViewSwitcher = false;\n        }\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    _checkValueLocalStorage(action) {\n        const actionIdStr = action.id.toString();\n        return this.state.embeddedInfos.visibleEmbeddedActions[actionIdStr];\n    }\n\n    /**\n     * The selected action is put into (or removed from) the localStorage and its visibility changes.\n     * The state variable visibleEmbeddedActions keeps track of the visible actions to avoid  having to parse\n     * the localStorage values every time we want to access them.\n     * @param {EmbeddedAction} action\n     */\n    _setVisibility(actionId) {\n        const actionIdStr = actionId.toString();\n        if (this.state.embeddedInfos.visibleEmbeddedActions[actionIdStr]) {\n            delete this.state.embeddedInfos.visibleEmbeddedActions[actionIdStr];\n        } else {\n            this.state.embeddedInfos.visibleEmbeddedActions[actionIdStr] = true;\n        }\n        browser.localStorage.setItem(\n            this.embeddedActionsVisibilityKey,\n            JSON.stringify(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 embeddedActionIdStr = embeddedActionId[0].toString();\n        visibleEmbeddedActions[embeddedActionIdStr] = true;\n        const order = this.state.embeddedInfos.embeddedActions.map((el) => el.id);\n        browser.localStorage.setItem(\n            this.embeddedActionsVisibilityKey,\n            JSON.stringify(visibleEmbeddedActions)\n        );\n        browser.localStorage.setItem(this.embeddedOrderKey, JSON.stringify(order));\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 actionIdStr = action.id.toString();\n        if (visibleEmbeddedActions[actionIdStr]) {\n            delete visibleEmbeddedActions[actionIdStr];\n        }\n        browser.localStorage.setItem(\n            this.embeddedActionsVisibilityKey,\n            JSON.stringify(visibleEmbeddedActions)\n        );\n        this.state.embeddedInfos.embeddedActions = embeddedActions.filter(\n            ({ id }) => id !== action.id\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            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: this.env.config.parentActionId ? \"replaceCurrentAction\" : \"\",\n            viewType: action.default_view_mode,\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        browser.localStorage.setItem(this.embeddedOrderKey, JSON.stringify(order));\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.notificationService = useService(\"notification\");\n        this.descriptionRef = useRef(\"description\");\n        this.state = useState({\n            description: this.env.config.getDisplayName(),\n            isDefault: false,\n            isShared: false,\n        });\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    saveFavorite(ev) {\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            return this.descriptionRef.el.focus();\n        }\n        const favorites = this.env.searchModel.getSearchItems(\n            (s) => s.type === \"favorite\" && s.description === this.state.description\n        );\n        if (favorites.length) {\n            this.notificationService.add(_t(\"A filter with same name already exists.\"), {\n                type: \"danger\",\n            });\n            ev.stopPropagation();\n            return this.descriptionRef.el.focus();\n        }\n        const { description, isDefault, isShared } = this.state;\n        const embeddedActionId = this.env.config.currentEmbeddedActionId || false;\n        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            isShared: false,\n        });\n    }\n\n    /**\n     * @param {boolean} checked\n     */\n    onDefaultCheckboxChange(checked) {\n        this.state.isDefault = checked;\n        if (checked) {\n            this.state.isShared = false;\n        }\n    }\n\n    /**\n     * @param {boolean} checked\n     */\n    onShareCheckboxChange(checked) {\n        this.state.isShared = checked;\n        if (checked) {\n            this.state.isDefault = false;\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        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 { exprToBoolean } from \"@web/core/utils/strings\";\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 getContextGroubBy(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            fold: false,\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                let value = this.searchDefaults[name];\n                value = Array.isArray(value) ? value[0] : value;\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                        return orm\n                            .call(relation, \"read\", [value, [\"display_name\"]], { context })\n                            .then((results) => {\n                                preField.defaultAutocompleteValue.label =\n                                    results[0][\"display_name\"];\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] = getContextGroubBy(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(\"fold\")) {\n            this.searchPanelInfo.fold = exprToBoolean(searchPanelNode.getAttribute(\"fold\"));\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.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, 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\";\n\nimport { Component, useExternalListener, useRef, useState } from \"@odoo/owl\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nconst parsers = registry.category(\"parsers\");\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    };\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    };\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        // core state\n        this.state = useState({\n            expanded: [],\n            focusedIndex: 0,\n            query: \"\",\n            subItemsLimits: {},\n        });\n\n        // derived state\n        this.items = useState([]);\n        this.subItems = {};\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();\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        useExternalListener(window, \"click\", this.onWindowClick);\n        useExternalListener(window, \"keydown\", this.onWindowKeydown);\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 {number} [options.focusedIndex]\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 focusedIndex =\n            \"focusedIndex\" in options ? options.focusedIndex : this.state.focusedIndex;\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.state.focusedIndex = focusedIndex;\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\n        if ([\"selection\", \"boolean\", \"tags\"].includes(fieldType)) {\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 = this.fields[searchItem.fieldName].selection || booleanOptions;\n            }\n            for (const [value, label] of options) {\n                if (fuzzyTest(trimmedQuery.toLowerCase(), label.toLowerCase())) {\n                    items.push({\n                        id: nextItemId++,\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        const parser = parsers.contains(fieldType) ? parsers.get(fieldType) : (str) => str;\n        let value;\n        try {\n            switch (fieldType) {\n                case \"date\": {\n                    value = serializeDate(parser(trimmedQuery));\n                    break;\n                }\n                case \"datetime\": {\n                    value = serializeDateTime(parser(trimmedQuery));\n                    break;\n                }\n                case \"many2one\": {\n                    value = trimmedQuery;\n                    break;\n                }\n                default: {\n                    value = parser(trimmedQuery);\n                }\n            }\n        } catch {\n            return [];\n        }\n\n        const item = {\n            id: nextItemId++,\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\") {\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        const context = { ...this.env.searchModel.domainEvalContext, ...field.context };\n        let domain = [];\n        if (searchItem.domain) {\n            try {\n                domain = new Domain(searchItem.domain).toList(context);\n            } catch {\n                // Pass\n            }\n        }\n        const relation =\n            searchItem.type === \"field_property\"\n                ? searchItem.propertyFieldDefinition.comodel\n                : field.relation;\n\n        let nameSearchOperator = \"ilike\";\n        if (query && query[0] === '\"' && query[query.length - 1] === '\"') {\n            query = query.slice(1, -1);\n            nameSearchOperator = \"=\";\n        }\n        const limitToFetch = this.state.subItemsLimits[searchItem.id] + 1;\n        const options = await this.orm.call(relation, \"name_search\", [], {\n            args: domain,\n            operator: nameSearchOperator,\n            context,\n            limit: limitToFetch,\n            name: query.trim(),\n        });\n\n        let showLoadMore = false;\n        if (options.length === limitToFetch) {\n            options.pop();\n            showLoadMore = true;\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: [], focusedIndex: 0, 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.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, label, operator, value } = item;\n            const autoCompleteValues = { label, operator, value };\n            if (value && value[0] === '\"' && value[value.length - 1] === '\"') {\n                autoCompleteValues.value = value.slice(1, -1);\n                autoCompleteValues.label = label.slice(1, -1);\n                autoCompleteValues.operator = \"=\";\n                autoCompleteValues.enforceEqual = true;\n            }\n            this.env.searchModel.addAutoCompletionValues(searchItemId, autoCompleteValues);\n        }\n\n        if (item.loadMore) {\n            item.loadMore();\n        } else {\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    //---------------------------------------------------------------------\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: (domain) => this.env.searchModel.splitAndAddDomain(domain, groupId),\n            disableConfirmButton: (domain) => domain === `[]`,\n            title: _t(\"Modify Condition\"),\n            isDebugMode: this.env.searchModel.isDebugMode,\n        });\n    }\n\n    /**\n     * @param {Object} facet\n     * @param {number} facetIndex\n     * @param {KeyboardEvent} ev\n     */\n    onFacetKeydown(facet, facetIndex, ev) {\n        switch (ev.key) {\n            case \"ArrowLeft\": {\n                if (facetIndex === 0) {\n                    this.inputRef.el.focus();\n                } else {\n                    this.focusFacet(facetIndex - 1);\n                }\n                break;\n            }\n            case \"ArrowRight\": {\n                const facets = this.root.el.getElementsByClassName(\"o_searchview_facet\");\n                if (facetIndex === facets.length - 1) {\n                    this.inputRef.el.focus();\n                } else {\n                    this.focusFacet(facetIndex + 1);\n                }\n                break;\n            }\n            case \"Backspace\": {\n                this.removeFacet(facet);\n                break;\n            }\n        }\n    }\n\n    /**\n     * @param {Object} facet\n     */\n    onFacetRemove(facet) {\n        this.removeFacet(facet);\n    }\n\n    /**\n     * @param {number} index\n     */\n    onItemMousemove(focusedIndex) {\n        this.state.focusedIndex = focusedIndex;\n        this.inputRef.el.focus();\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onSearchKeydown(ev) {\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 focusedItem = this.items[this.state.focusedIndex];\n        let focusedIndex;\n        switch (ev.key) {\n            case \"ArrowDown\":\n                ev.preventDefault();\n                if (this.items.length) {\n                    if (this.state.focusedIndex >= this.items.length - 1) {\n                        focusedIndex = 0;\n                    } else {\n                        focusedIndex = this.state.focusedIndex + 1;\n                    }\n                } else {\n                    this.env.searchModel.trigger(\"focus-view\");\n                }\n                break;\n            case \"ArrowUp\":\n                ev.preventDefault();\n                if (this.items.length) {\n                    if (\n                        this.state.focusedIndex === 0 ||\n                        this.state.focusedIndex > this.items.length - 1\n                    ) {\n                        focusedIndex = this.items.length - 1;\n                    } else {\n                        focusedIndex = this.state.focusedIndex - 1;\n                    }\n                }\n                break;\n            case \"ArrowLeft\":\n                if (focusedItem && focusedItem.isParent && focusedItem.isExpanded) {\n                    ev.preventDefault();\n                    this.toggleItem(focusedItem, false);\n                } else if (focusedItem && focusedItem.isChild) {\n                    ev.preventDefault();\n                    focusedIndex = this.items.findIndex(\n                        (item) => item.isParent && item.searchItemId === focusedItem.searchItemId\n                    );\n                } else if (focusedItem && focusedItem.isFieldProperty) {\n                    ev.preventDefault();\n                    focusedIndex = this.items.findIndex(\n                        (item) => item.isParent && item.searchItemId === focusedItem.propertyItemId\n                    );\n                } else if (ev.target.selectionStart === 0) {\n                    // focus rightmost facet if any.\n                    this.focusFacet();\n                } else {\n                    // do nothing and navigate inside text\n                }\n                break;\n            case \"ArrowRight\":\n                if (ev.target.selectionStart === this.state.query.length) {\n                    if (focusedItem && focusedItem.isParent) {\n                        ev.preventDefault();\n                        if (focusedItem.isExpanded) {\n                            focusedIndex = this.state.focusedIndex + 1;\n                        } else {\n                            this.toggleItem(focusedItem, true);\n                        }\n                    } else if (ev.target.selectionStart === this.state.query.length) {\n                        // Priority 3: focus leftmost facet if any.\n                        this.focusFacet(0);\n                    }\n                }\n                break;\n            case \"Backspace\":\n                if (!this.state.query.length) {\n                    const facets = this.env.searchModel.facets;\n                    if (facets.length) {\n                        this.removeFacet(facets[facets.length - 1]);\n                    }\n                }\n                break;\n            case \"Enter\":\n                if (!this.state.query.length) {\n                    this.env.searchModel.search(); /** @todo keep this thing ?*/\n                    break;\n                } else if (focusedItem) {\n                    ev.preventDefault(); // keep the focus inside the search bar\n                    this.selectItem(focusedItem);\n                }\n                break;\n            case \"Tab\":\n                if (this.state.query.length && focusedItem) {\n                    ev.preventDefault(); // keep the focus inside the search bar\n                    this.selectItem(focusedItem);\n                }\n                break;\n            case \"Escape\":\n                this.resetState();\n                break;\n        }\n\n        if (focusedIndex !== undefined) {\n            this.state.focusedIndex = focusedIndex;\n        }\n    }\n\n    onSearchClick() {\n        if (!hasTouch() && !this.inputRef.el.value.length) {\n            this.searchBarDropdownState.open();\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            this.computeState({ query, expanded: [], focusedIndex: 0, subItems: [] });\n        } else if (this.items.length) {\n            this.resetState();\n        }\n    }\n\n    onClickSearchIcon() {\n        const focusedItem = this.items[this.state.focusedIndex];\n        if (!this.state.query.length) {\n            this.env.searchModel.search();\n        } else if (focusedItem) {\n            this.selectItem(focusedItem);\n        }\n    }\n\n    onToggleSearchBar() {\n        this.state.showSearchBar = !this.state.showSearchBar;\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    onWindowClick(ev) {\n        if (this.items.length && !this.root.el.contains(ev.target)) {\n            this.resetState({ focus: false });\n        }\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onWindowKeydown(ev) {\n        if (this.items.length && ev.key === \"Escape\") {\n            this.resetState();\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 } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\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\";\nimport { _t } from \"@web/core/l10n/translation\";\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: {\n            type: Object,\n            optional: true,\n            shape: {\n                isOpen: Boolean,\n                open: Function,\n                close: Function,\n            },\n        },\n    };\n\n    setup() {\n        this.facet_icons = FACET_ICONS;\n        // Filter\n        this.dialogService = useService(\"dialog\");\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        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    // Comparison Panel\n    get showComparisonMenu() {\n        return (\n            this.env.searchModel.searchMenuTypes.has(\"comparison\") &&\n            this.env.searchModel.getSearchItems((i) => i.type === \"comparison\").length > 0\n        );\n    }\n    get comparisonItems() {\n        return this.env.searchModel.getSearchItems(\n            (searchItem) => searchItem.type === \"comparison\"\n        );\n    }\n\n    /**\n     * @param {number} itemId\n     */\n    onComparisonSelected(itemId) {\n        this.env.searchModel.toggleSearchItem(itemId);\n    }\n\n    // Favorite Panel\n\n    get favorites() {\n        return this.env.searchModel.getSearchItems(\n            (searchItem) => searchItem.type === \"favorite\" && searchItem.userId !== false\n        );\n    }\n\n    get sharedFavorites() {\n        return this.env.searchModel.getSearchItems(\n            (searchItem) => searchItem.type === \"favorite\" && searchItem.userId === false\n        );\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    openConfirmationDialog(itemId, userId) {\n        const dialogProps = {\n            title: _t(\"Warning\"),\n            body: userId\n                ? _t(\"Are you sure that you want to remove this filter?\")\n                : _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 { makeContext } from \"@web/core/context\";\nimport { Domain } from \"@web/core/domain\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { user } from \"@web/core/user\";\nimport { sortBy, groupBy } 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    getComparisonOptions,\n    getIntervalOptions,\n    getPeriodOptions,\n    rankInterval,\n    yearSelected,\n} from \"./utils/dates\";\nimport { FACET_ICONS, FACET_COLORS } from \"./utils/misc\";\n\nimport { EventBus, toRaw } from \"@odoo/owl\";\nimport { domainFromTree, treeFromDomain } from \"@web/core/tree_editor/condition_tree\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useGetTreeDescription, useMakeGetFieldDef } from \"@web/core/tree_editor/utils\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { getDefaultDomain } from \"@web/core/domain_selector/utils\";\n\nconst { DateTime } = luxon;\n\n/** @typedef {import(\"@web/core/domain\").DomainRepr} DomainRepr */\n/** @typedef {import(\"@web/core/domain\").DomainListRepr} DomainListRepr */\n/** @typedef {import(\"../views/utils\").OrderTerm} OrderTerm */\n\n/**\n * @typedef {Object} ComparisonDomain\n * @property {DomainListRepr} arrayRepr\n * @property {string} description\n */\n\n/**\n * @typedef {Object} Comparison\n * @property {ComparisonDomain[]} domains\n * @property {string} [fieldName]\n */\n\n/**\n * @typedef {Object} SearchParams\n * @property {Comparison | null} comparison\n * @property {Context} context\n * @property {DomainListRepr} domain\n * @property {string[]} groupBy\n * @property {OrderTerm[]} orderBy\n * @property {boolean} [useSampleModel] to remove?\n */\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     * @override\n     */\n    setup(services) {\n        // services\n        const { field: fieldService, name: nameService, orm, view, dialog } = services;\n        this.orm = orm;\n        this.fieldService = fieldService;\n        this.viewService = view;\n        this.dialog = dialog;\n        this.orderByCount = false;\n\n        this.getDomainTreeDescription = useGetTreeDescription(fieldService, nameService);\n        this.makeGetFieldDef = useMakeGetFieldDef(fieldService);\n\n        // used to manage search items related to date/datetime fields\n        this.referenceMoment = DateTime.local();\n        this.comparisonOptions = getComparisonOptions();\n        this.intervalOptions = getIntervalOptions();\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 | null} [config.comparison]\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(`SearchPanel config should have a \"resModel\" key`);\n        }\n        this.resModel = resModel;\n\n        // used to avoid useless recomputations\n        this._reset();\n\n        const { comparison, context, domain, groupBy, hideCustomGroupBy, orderBy } = config;\n\n        this.globalComparison = comparison;\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\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            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 dateFilters = Object.values(this.searchItems).filter(\n            (searchElement) => searchElement.type === \"dateFilter\"\n        );\n        if (dateFilters.length) {\n            this._createGroupOfComparisons(dateFilters);\n        }\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 | null} [config.comparison]\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 { comparison, context, domain, groupBy, orderBy } = config;\n\n        this.globalContext = Object.assign({}, context);\n        this.globalDomain = domain || [];\n        this.globalComparison = comparison;\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    /**\n     * @returns {Comparison}\n     */\n    get comparison() {\n        if (!this.searchMenuTypes.has(\"comparison\")) {\n            return null;\n        }\n        if (this._comparison === undefined) {\n            if (this.globalComparison) {\n                this._comparison = this.globalComparison;\n            } else {\n                const comparison = this.getFullComparison();\n                if (comparison) {\n                    const {\n                        fieldName,\n                        range,\n                        rangeDescription,\n                        comparisonRange,\n                        comparisonRangeDescription,\n                    } = comparison;\n                    const domains = [\n                        {\n                            arrayRepr: Domain.and([this.domain, range]).toList(),\n                            description: rangeDescription,\n                        },\n                        {\n                            arrayRepr: Domain.and([this.domain, comparisonRange]).toList(),\n                            description: comparisonRangeDescription,\n                        },\n                    ];\n                    this._comparison = { domains, fieldName };\n                } else {\n                    this._comparison = null;\n                }\n            }\n        }\n        return deepCopy(this._comparison);\n    }\n\n    get facets() {\n        const isValidType = (type) =>\n            ![\"groupBy\", \"comparison\"].includes(type) || this.searchMenuTypes.has(type);\n        const facets = [];\n        for (const facet of this._getFacets()) {\n            if (!isValidType(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     * 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: preFavorite.userId ? 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    }\n\n    async _createIrFilters(irFilter) {\n        const serverSideId = await this.orm.call(\"ir.filters\", \"create_or_replace\", [irFilter]);\n        this.env.bus.trigger(\"CLEAR-CACHES\");\n        return serverSideId;\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        this.query = this.query.filter((queryElem) => {\n            const searchItem = this.searchItems[queryElem.searchItemId];\n            return searchItem.groupId !== groupId;\n        });\n        this._checkComparisonStatus();\n        this._checkOrderByCountStatus();\n        this._notify();\n    }\n\n    /**\n     * Delete a filter of type 'favorite' with given this.nextId server side and\n     * in control panel model. Of course the filter is also removed\n     * from the search query.\n     */\n    async deleteFavorite(favoriteId) {\n        const searchItem = this.searchItems[favoriteId];\n        if (searchItem.type !== \"favorite\") {\n            return;\n        }\n        await 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    async _deleteIrFilters(searchItem) {\n        const { serverSideId } = searchItem;\n        await this.orm.unlink(\"ir.filters\", [serverSideId]);\n        this.env.bus.trigger(\"CLEAR-CACHES\");\n    }\n\n    /**\n     * @returns {Object}\n     */\n    exportState() {\n        const state = {};\n        execute(mapToArray, this, state);\n        return state;\n    }\n\n    getFullComparison() {\n        let searchItem = null;\n        for (const queryElem of this.query.slice().reverse()) {\n            const item = this.searchItems[queryElem.searchItemId];\n            if (item.type === \"comparison\") {\n                searchItem = item;\n                break;\n            } else if (item.type === \"favorite\" && item.comparison) {\n                searchItem = item;\n                break;\n            }\n        }\n        if (!searchItem) {\n            return null;\n        } else if (searchItem.type === \"favorite\") {\n            return searchItem.comparison;\n        }\n        const { dateFilterId, comparisonOptionId } = searchItem;\n        const dateFilter = this.searchItems[dateFilterId];\n        const { fieldName, description: dateFilterDescription } = dateFilter;\n        const selectedGeneratorIds = this._getSelectedGeneratorIds(dateFilterId);\n        // compute range and range description\n        const { domain: range, description: rangeDescription } = constructDateDomain(\n            this.referenceMoment,\n            dateFilter,\n            selectedGeneratorIds\n        );\n        // compute comparisonRange and comparisonRange description\n        const { domain: comparisonRange, description: comparisonRangeDescription } =\n            constructDateDomain(\n                this.referenceMoment,\n                dateFilter,\n                selectedGeneratorIds,\n                comparisonOptionId\n            );\n        return {\n            comparisonId: comparisonOptionId,\n            fieldName,\n            fieldDescription: dateFilterDescription,\n            range: range.toList(),\n            rangeDescription,\n            comparisonRange: comparisonRange.toList(),\n            comparisonRangeDescription,\n        };\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.globalContext);\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 getFieldDef = await this.makeGetFieldDef(this.resModel, treeFromDomain(domain));\n        const tree = treeFromDomain(domain, { distributeNot: !this.isDebugMode, getFieldDef });\n        const trees = !tree.negate && tree.value === \"&\" ? tree.children : [tree];\n        const promises = trees.map(async (tree) => {\n            const description = await this.getDomainTreeDescription(this.resModel, tree);\n            const preFilter = {\n                description,\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        if (group) {\n            const firstActiveItem = group.activeItems[0];\n            const firstSearchItem = this.searchItems[firstActiveItem.searchItemId];\n            const { type } = firstSearchItem;\n            if (type === \"favorite\") {\n                const activeItemGroupBys = this._getSearchItemGroupBys(firstActiveItem);\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            this.deactivateGroup(groupId);\n        }\n\n        for (const preFilter of preFilters) {\n            this.createNewFilters([preFilter]);\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            } else if (searchItem.type === \"comparison\") {\n                // make sure only one comparison can be active\n                this.query = this.query.filter((queryElem) => {\n                    const { type } = this.searchItems[queryElem.searchItemId];\n                    return type !== \"comparison\";\n                });\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                    const comparisonId = this._getActiveComparison()?.id;\n                    this.query = this.query.filter(\n                        (queryElem) =>\n                            ![searchItemId, comparisonId].includes(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._checkComparisonStatus();\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.domainEvalContext,\n            onConfirm: (domain) => this.splitAndAddDomain(domain),\n            disableConfirmButton: (domain) => domain === `[]`,\n            title: _t(\"Add Custom Filter\"),\n            confirmButtonText: _t(\"Add\"),\n            discardButtonText: _t(\"Cancel\"),\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: `${definition.string} (${definitionRecordName})`,\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} (${definitionRecordName})`,\n                        type: definition.type,\n                        relatedPropertyField: field,\n                    };\n\n                    if (!searchItemsNames.includes(fullName)) {\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            return {\n                definitionRecordId: parseInt(recordId),\n                definitionRecordName: definitions[0]?.record_name,\n                definitions,\n            };\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    /**\n     * If a comparison is active, check if it should become inactive.\n     * The comparison should become inactive if the corresponding date filter has become\n     * inactive.\n     */\n    _checkComparisonStatus() {\n        const activeComparison = this._getActiveComparison();\n        if (!activeComparison) {\n            return;\n        }\n        const { dateFilterId, id } = activeComparison;\n        const dateFilterIsActive = this.query.some(\n            (queryElem) => queryElem.searchItemId === dateFilterId\n        );\n        if (!dateFilterIsActive) {\n            this.query = this.query.filter((queryElem) => queryElem.searchItemId !== id);\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     * Starting from the array of date filters, create the filters of type\n     * 'comparison'.\n     * @param {Object[]} dateFilters\n     */\n    _createGroupOfComparisons(dateFilters) {\n        const preSearchItem = [];\n        for (const dateFilter of dateFilters) {\n            for (const comparisonOption of this.comparisonOptions) {\n                const { id: dateFilterId, description } = dateFilter;\n                const preFilter = {\n                    type: \"comparison\",\n                    comparisonOptionId: comparisonOption.id,\n                    description: `${description}: ${comparisonOption.description}`,\n                    dateFilterId,\n                };\n                preSearchItem.push(preFilter);\n            }\n        }\n        this._createGroupOfSearchItems(preSearchItem);\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            return {\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        });\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 \"comparison\": {\n                const { dateFilterId } = searchItem;\n                const dateFilterIsActive = this.query.some(\n                    (queryElem) =>\n                        queryElem.searchItemId === dateFilterId &&\n                        !queryElem.generatorId.startsWith(\"custom\")\n                );\n                if (!dateFilterIsActive) {\n                    return null;\n                }\n                break;\n            }\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        await Promise.all(\n            categories.map(async (category) => {\n                const result = await this.orm.call(\n                    this.resModel,\n                    \"search_panel_select_range\",\n                    [category.fieldName],\n                    {\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                );\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        await Promise.all(\n            filters.map(async (filter) => {\n                const result = await this.orm.call(\n                    this.resModel,\n                    \"search_panel_select_multi_range\",\n                    [filter.fieldName],\n                    {\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                );\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    _getActiveComparison() {\n        for (const queryElem of this.query) {\n            const searchItem = this.searchItems[queryElem.searchItemId];\n            if (searchItem.type === \"comparison\") {\n                return searchItem;\n            }\n        }\n        return null;\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        let context;\n        try {\n            context = makeContext(contexts);\n            return context;\n        } catch (error) {\n            throw new Error(\n                _t(\"Failed to evaluate the context: %(context)s.\\n%(error)s\", {\n                    context,\n                    error: error.message,\n                })\n            );\n        }\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 { bannerRoute, 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            banner: Boolean(bannerRoute),\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        let domain;\n        try {\n            domain = Domain.and(domains);\n            return params.raw ? domain : domain.toList(this.domainEvalContext);\n        } catch (error) {\n            throw new Error(\n                _t(\"Failed to evaluate the domain: %(domain)s.\\n%(error)s\", {\n                    domain: domain.toString(),\n                    error: error.message,\n                })\n            );\n        }\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            for (const activeItem of group.activeItems) {\n                const domain = this._getSearchItemDomain(activeItem, {\n                    withDateFilterDomain: true,\n                });\n                if (domain) {\n                    groupActiveItemDomains.push(domain);\n                }\n                const searchItem = this.searchItems[activeItem.searchItemId];\n                switch (searchItem.type) {\n                    case \"field_property\":\n                    case \"field\": {\n                        type = \"field\";\n                        title = searchItem.description;\n                        for (const autocompleteValue of activeItem.autocompletValues) {\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 option = this.intervalOptions.find((o) => o.id === intervalId);\n                            values.push(`${searchItem.description}: ${option.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                        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 (groupActiveItemDomains.length) {\n                facet.domain = Domain.or(groupActiveItemDomains).toString();\n            }\n            facets.push(facet);\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, enforceEqual }) => {\n            let domain;\n            if (field.filterDomain) {\n                let filterDomain = field.filterDomain;\n                if (enforceEqual) {\n                    filterDomain = field.filterDomain\n                        .replaceAll(\"'ilike'\", \"'='\")\n                        .replaceAll('\"ilike\"', '\"=\"');\n                }\n                domain = new Domain(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 grouBys in this.globalContext.\n     */\n    _getGroupBy() {\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 ? groupBys : this.globalGroupBy.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, autocompletValues: [] };\n                        activeItems.push(activeItem);\n                    }\n                    activeItem.autocompletValues.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 comparison = this.getFullComparison();\n        const orderBy = localOrderBy || this._getOrderBy();\n        const userId = isShared ? false : user.userId;\n\n        const preFavorite = {\n            description,\n            isDefault,\n            domain,\n            context,\n            groupBys,\n            orderBy,\n            userId,\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_id: userId,\n            context: { group_by: groupBys, ...context },\n        };\n\n        if (comparison) {\n            preFavorite.comparison = comparison;\n            irFilter.context.comparison = comparison;\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                    try {\n                        const self = activeItem.autocompletValues.map(\n                            (autocompleValue) => autocompleValue.value\n                        );\n                        context = evaluateExpr(searchItem.context, { self });\n                        if (typeof context !== \"object\") {\n                            throw Error();\n                        }\n                    } catch (error) {\n                        throw new Error(\n                            _t(\"Failed to evaluate the context: %(context)s.\\n%(error)s\", {\n                                context: searchItem.context,\n                                error: error.message,\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     * @param {Object} [options={}]\n     * @param {boolean} [options.withDateFilterDomain]\n     */\n    _getSearchItemDomain(activeItem, options = {}) {\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.autocompletValues);\n            }\n            case \"dateFilter\": {\n                const { dateFilterId } = this._getActiveComparison() || {};\n                if (\n                    options.withDateFilterDomain ||\n                    !(this.searchMenuTypes.has(\"comparison\") && dateFilterId === searchItemId)\n                ) {\n                    return this._getDateFilterDomain(searchItem, activeItem.generatorIds);\n                }\n                return new Domain([]);\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        let userId = false;\n        if (Array.isArray(irFilter.user_id)) {\n            userId = irFilter.user_id[0];\n        }\n        const groupNumber = userId ? 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 comparison;\n        if (context.comparison) {\n            comparison = context.comparison;\n            if (typeof comparison.range === \"string\") {\n                // legacy case\n                comparison.range = new Domain(comparison.range).toList();\n            }\n            if (typeof comparison.comparisonRange === \"string\") {\n                // legacy case\n                comparison.comparisonRange = new Domain(comparison.comparisonRange).toList();\n            }\n            delete context.comparison;\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            userId,\n        };\n        if (irFilter.is_default) {\n            favorite.isDefault = irFilter.is_default;\n        }\n        if (comparison) {\n            favorite.comparison = comparison;\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     * 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        delete this._comparison;\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 { 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.state = useState({\n            active: {},\n            expanded: {},\n            showMobileSearch: false,\n            sidebarExpanded: !this.env.searchModel.searchPanelInfo.fold,\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\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                return {\n                    searchPanel: this.exportState(),\n                };\n            },\n        });\n\n        onWillStart(async () => {\n            await this.env.searchModel.sectionsPromise;\n            this.expandDefaultValue();\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    /**\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    }\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        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\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// ComparisonMenu parameters\nexport const COMPARISON_OPTIONS = {\n    previous_period: {\n        description: _t(\"Previous Period\"),\n        id: \"previous_period\",\n    },\n    previous_year: {\n        description: _t(\"Previous Year\"),\n        id: \"previous_year\",\n        plusParam: { years: -1 },\n    },\n};\n\nexport const PER_YEAR = {\n    year: 1,\n    quarter: 4,\n    month: 12,\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(\n    referenceMoment,\n    searchItem,\n    selectedOptionIds,\n    comparisonOptionId\n) {\n    let plusParam;\n    let selectedOptions;\n    if (comparisonOptionId) {\n        [plusParam, selectedOptions] = getComparisonParams(\n            referenceMoment,\n            searchItem,\n            selectedOptionIds,\n            comparisonOptionId\n        );\n    } else {\n        selectedOptions = getSelectedOptions(referenceMoment, searchItem, selectedOptionIds);\n    }\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 COMPARISON_OPTIONS with translated descriptions.\n * @see getOptionsWithDescriptions\n */\nexport function getComparisonOptions() {\n    return getOptionsWithDescriptions(COMPARISON_OPTIONS);\n}\n\n/**\n * Returns the params plusParam and selectedOptions necessary for the computation\n * of a comparison domain.\n */\nexport function getComparisonParams(\n    referenceMoment,\n    searchItem,\n    selectedOptionIds,\n    comparisonOptionId\n) {\n    const comparisonOption = COMPARISON_OPTIONS[comparisonOptionId];\n    const selectedOptions = getSelectedOptions(referenceMoment, searchItem, selectedOptionIds);\n    if (comparisonOption.plusParam) {\n        return [comparisonOption.plusParam, selectedOptions];\n    }\n    const plusParam = {};\n    let globalGranularity = \"year\";\n    if (selectedOptions.month) {\n        globalGranularity = \"month\";\n    } else if (selectedOptions.quarter) {\n        globalGranularity = \"quarter\";\n    }\n    const granularityFactor = PER_YEAR[globalGranularity];\n    const years = selectedOptions.year.map((o) => o.setParam.year);\n    const yearMin = Math.min(...years);\n    const yearMax = Math.max(...years);\n    let optionMin = 0;\n    let optionMax = 0;\n    if (selectedOptions.quarter) {\n        const quarters = selectedOptions.quarter.map((o) => o.setParam.quarter);\n        if (globalGranularity === \"month\") {\n            delete selectedOptions.quarter;\n            for (const quarter of quarters) {\n                for (const month of QUARTERS[quarter].coveredMonths) {\n                    const monthOption = selectedOptions.month.find(\n                        (o) => o.setParam.month === month\n                    );\n                    if (!monthOption) {\n                        selectedOptions.month.push({\n                            setParam: { month },\n                            granularity: \"month\",\n                        });\n                    }\n                }\n            }\n        } else {\n            optionMin = Math.min(...quarters);\n            optionMax = Math.max(...quarters);\n        }\n    }\n    if (selectedOptions.month) {\n        const months = selectedOptions.month.map((o) => o.setParam.month);\n        optionMin = Math.min(...months);\n        optionMax = Math.max(...months);\n    }\n    const num = -1 + granularityFactor * (yearMin - yearMax) + optionMin - optionMax;\n    const key =\n        globalGranularity === \"year\"\n            ? \"years\"\n            : globalGranularity === \"month\"\n            ? \"months\"\n            : \"quarters\";\n    plusParam[key] = num;\n    return [plusParam, selectedOptions];\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, 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]) {\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(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(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    comparison: \"fa fa-adjust\",\n};\n\nexport const FACET_COLORS = {\n    filter: \"primary\",\n    groupBy: \"action\",\n    favorite: \"warning\",\n    comparison: \"danger\",\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 {Object} OrderTerm\n * @property {string} name\n * @property {boolean} asc\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 = [\"comparison\", \"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        comparison: { type: [Object, { value: null }], optional: true },\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    };\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            },\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                return {\n                    searchModel: JSON.stringify(this.searchModel.exportState()),\n                };\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 { 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\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/** @typedef {Object} Config\n *  @property {integer|false} actionId\n *  @property {string|false} actionType\n *  @property {Object} actionFlags\n *  @property {() => []} breadcrumbs\n *  @property {() => string} getDisplayName\n *  @property {(string) => void} setDisplayName\n *  @property {() => Object} getPagerProps\n *  @property {Object[]} viewSwitcherEntry\n *  @property {Object[]} viewSwitcherEntry\n *  @property {Component} Banner\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        embeddedActions: [],\n        currentEmbeddedActionId: false,\n        parentActionId: false,\n        actionFlags: {},\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\n/** @typedef {import(\"./utils\").OrderTerm} OrderTerm */\n\n/** @typedef {Object} ViewProps\n *  @property {string} resModel\n *  @property {string} type\n *\n *  @property {string} [arch] if given, fields must be given too /\\ no post processing is done (evaluation of \"groups\" attribute,...)\n *  @property {Object} [fields] if given, arch must be given too\n *  @property {number|false} [viewId]\n *  @property {Object} [actionMenus]\n *  @property {boolean} [loadActionMenus=false]\n *\n *  @property {string} [searchViewArch] if given, searchViewFields must be given too\n *  @property {Object} [searchViewFields] if given, searchViewArch must be given too\n *  @property {number|false} [searchViewId]\n *  @property {Object[]} [irFilters]\n *  @property {boolean} [loadIrFilters=false]\n *\n *  @property {Object} [comparison]\n *  @property {Object} [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 {Object} [display={}] to rework\n *\n *  manipulated by withSearch\n *\n *  @property {boolean} [activateFavorite]\n *  @property {Object[]} [dynamicFilters]\n *  @property {boolean} [hideCustomGroupBy]\n *  @property {string[]} [searchMenuTypes]\n *  @property {Object} [globalState]\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    \"comparison\",\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\"];\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    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 result = await this.viewService.loadViews(\n                { context, resModel, views },\n                {\n                    actionId: this.env.config.actionId,\n                    embeddedActionId: this.env.config.currentEmbeddedActionId,\n                    embeddedParentResId: context.active_id,\n                    loadActionMenus,\n                    loadIrFilters,\n                }\n            );\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        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        for (const key in this.withSearchProps) {\n            if (!(key in WithSearch.props)) {\n                delete this.withSearchProps[key];\n            }\n        }\n    }\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        // comparison, context, domain, groupBy, orderBy\n        const { comparison, context, domain, groupBy, orderBy } = nextProps;\n        Object.assign(this.withSearchProps, { comparison, context, domain, groupBy, orderBy });\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\n\nimport { useComponent, useEffect, xml } from \"@odoo/owl\";\n\nexport function useViewArch(arch, params = {}) {\n    const CATEGORY = \"__processed_archs__\";\n\n    arch = arch.trim();\n    const processedRegistry = registry.category(CATEGORY);\n\n    let processedArch;\n    if (!processedRegistry.contains(arch)) {\n        processedArch = {};\n        processedRegistry.add(arch, processedArch);\n    } else {\n        processedArch = processedRegistry.get(arch);\n    }\n\n    const { compile, extract } = params;\n    if (!(\"template\" in processedArch) && compile) {\n        processedArch.template = xml`${compile(arch)}`;\n    }\n    if (!(\"extracted\" in processedArch) && extract) {\n        processedArch.extracted = extract(arch);\n    }\n\n    return processedArch;\n}\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", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { DebugMenu } from \"@web/core/debug/debug_menu\";\nimport { useOwnDebugContext } from \"@web/core/debug/debug_context\";\n\nimport { useEffect } from \"@odoo/owl\";\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        useEffect(\n            () => {\n                if (this.modalRef.el.querySelector(\".modal-footer\")?.childElementCount > 1) {\n                    const defaultButton = this.modalRef.el.querySelector(\n                        \".modal-footer button.o-default-button\"\n                    );\n                    defaultButton.classList.add(\"d-none\");\n                }\n            },\n            () => []\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 { 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 { registry } from \"@web/core/registry\";\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 if (name === \"data-hotkey\") {\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 * 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", "import { formatDate as _formatDate, formatDateTime as _formatDateTime } 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 { escape, 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.escape=false] if true, escapes the formatted value\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    if (options && options.escape) {\n        value = escape(value);\n    }\n    return value;\n}\nformatChar.extractOptions = ({ attrs }) => {\n    return {\n        isPassword: exprToBoolean(attrs.password),\n    };\n};\n\nexport function formatDate(value, options) {\n    return _formatDate(value, options);\n}\nformatDate.extractOptions = ({ options }) => {\n    return { condensed: options.condensed };\n};\n\nexport function formatDateTime(value, options = {}) {\n    if (options.showTime === false) {\n        return _formatDate(value, options);\n    }\n    return _formatDateTime(value, options);\n}\nformatDateTime.extractOptions = ({ attrs, options }) => {\n    return {\n        ...formatDate.extractOptions({ attrs, options }),\n        showSeconds: exprToBoolean(options.show_seconds ?? true),\n        showTime: exprToBoolean(options.show_time ?? true),\n    };\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    return { decimals, digits, humanReadable };\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    return {\n        ...formatFloat.extractOptions({ attrs, options }),\n        factor: options.factor,\n    };\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    return {\n        displaySeconds: options.displaySeconds,\n    };\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    return {\n        decimals: options.decimals || 0,\n        humanReadable: !!options.human_readable,\n        isPassword: exprToBoolean(attrs.password),\n    };\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]. The returned\n * value will then be the display name of the given value, or an empty string\n * if the value is false.\n *\n * @param {[number, 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 (value[1]) {\n        value = 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 = Array.isArray(dataValue) ? dataValue[0] : dataValue;\n    }\n    return formatCurrency(value, currencyId, options);\n}\nformatMonetary.extractOptions = ({ options }) => {\n    return {\n        noSymbol: options.no_symbol,\n        currencyField: options.currency_field,\n    };\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(value ? [value.resId, value.displayName] : false, options);\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([value.resId, 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 { 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        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    };\n    static defaultProps = {\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        if (!ev.target.files.length) {\n            return;\n        }\n        const { target } = ev;\n        for (const file of ev.target.files) {\n            if (!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    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", "export * from \"./store\";\nexport * from \"./record\";\nexport * from \"./make_store\";\nexport { AND, OR } 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    Store.env = env;\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            env,\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 { markup } from \"@odoo/owl\";\nimport { 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\n/**\n * Class of markup, useful to detect content that is markup and to\n * automatically markup field during trusted insert\n */\nexport const Markup = markup(\"\").constructor;\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\");\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", "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 { toRaw } from \"@odoo/owl\";\nimport {\n    ATTR_SYM,\n    FIELD_DEFINITION_SYM,\n    IS_DELETED_SYM,\n    MANY_SYM,\n    ONE_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(\"./misc\").RecordField} RecordField */\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\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    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 Record.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 Record.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 Record.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 Record.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    /**\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    static 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    static 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 {boolean} [param1.html] if set, the field value contains html value.\n     *   Useful to automatically markup when the insert is trusted.\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    static attr(def, param1) {\n        return { ...param1, [FIELD_DEFINITION_SYM]: true, [ATTR_SYM]: true, default: def };\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 oldTrusted = store._.trusted;\n            store._.trusted = options.html ?? store._.trusted;\n            const res = data.map(function RecordInsertMap(d) {\n                return Model._insert.call(ModelFullProxy, d, options);\n            });\n            store._.trusted = oldTrusted;\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    get localId() {\n        return toRaw(this)._.localId;\n    }\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                // 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    toData() {\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            if (isMany(Model, name)) {\n                data[name] = record._proxyInternal[name].map((recordProxy) => {\n                    const record = toRaw(recordProxy)._raw;\n                    return record.toIdData.call(record._proxyInternal);\n                });\n            } else if (isOne(Model, name)) {\n                const otherRecord = toRaw(record._proxyInternal[name])?._raw;\n                data[name] = otherRecord?.toIdData.call(otherRecord._proxyInternal);\n            } else {\n                // Record.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 {\n                    data[name] = value;\n                }\n            }\n        }\n        delete data._;\n        delete data._fieldsValue;\n        delete data._proxy;\n        delete data._proxyInternal;\n        delete data._raw;\n        delete data.Model;\n        return data;\n    }\n    toIdData() {\n        const data = this.Model._retrieveIdFromData(this);\n        for (const [name, val] of Object.entries(data)) {\n            if (isRecord(val)) {\n                data[name] = val.toIdData();\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_DELETING_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    [IS_DELETED_SYM] = false;\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_DELETING_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_DELETING_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        store._.updateFields(record, {\n            [fieldName]: Model._.fieldsCompute\n                .get(fieldName)\n                .call(this.fieldsComputeProxy2.get(fieldName)),\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            store._.sortRecordList(proxy2Sort[fieldName]._proxy, func);\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 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        Model._.fieldsOnUpdate.get(fieldName).call(record._proxyInternal);\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            // 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(recordList._store.recordByLocalId).get(\n                                    recordList.data[index]\n                                );\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);\n                                }\n                                recordListProxy.data[index] = newRecord?.localId;\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);\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 { IS_DELETED_SYM, STORE_SYM } from \"./misc\";\nimport { reactive, toRaw } from \"@odoo/owl\";\n\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\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    /** @param {() => any} fn */\n    MAKE_UPDATE(fn) {\n        this._.UPDATE++;\n        const res = fn();\n        this._.UPDATE--;\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                            onAdd?.call(record._proxy, addedRec._proxy);\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                            onDelete?.call(record._proxy, removedRec._proxy);\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                    cb();\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 usingRecord2 = toRaw(this.recordByLocalId).get(localId);\n                            if (!usingRecord2) {\n                                // record already deleted, clean inverses\n                                record._.uses.data.delete(localId);\n                                continue;\n                            }\n                            if (usingRecord2.Model._.fieldsMany.get(name2)) {\n                                for (let c = 0; c < count; c++) {\n                                    usingRecord2[name2].delete(record);\n                                }\n                            } else {\n                                usingRecord2[name2] = undefined;\n                            }\n                        }\n                    }\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                    record._[IS_DELETED_SYM] = true;\n                    delete record.Model.records[record.localId];\n                    this.recordByLocalId.delete(record.localId);\n                }\n            }\n            this._.UPDATE--;\n        }\n        return res;\n    }\n    onChange(record, name, cb) {\n        return this._onChange(record, name, (observe) => {\n            const fn = () => {\n                observe();\n                cb();\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}\n", "/** @typedef {import(\"./record\").Record} Record */\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\n\nimport { markup, toRaw } from \"@odoo/owl\";\nimport { RecordInternal } from \"./record_internal\";\nimport { deserializeDate, deserializeDateTime } from \"@web/core/l10n/dates\";\nimport { IS_DELETING_SYM, Markup, isCommand, isMany } from \"./misc\";\n\nexport class StoreInternal extends RecordInternal {\n    /**\n     * Determines whether the inserts are considered trusted or not.\n     * Useful to auto-markup html fields when this is set\n     */\n    trusted = false;\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    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                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 && this.trusted) {\n            shouldChange =\n                record[fieldName]?.toString() !== value?.toString() ||\n                !(record[fieldName] instanceof Markup);\n            newValue = typeof value === \"string\" ? markup(value) : value;\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        for (const [fieldName, value] of Object.entries(vals)) {\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 { Component, useState } from \"@odoo/owl\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\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 ImageActions extends Component {\n    static components = { Dropdown, DropdownItem };\n    static props = [\"actions\", \"imagesHeight\"];\n    static template = \"mail.ImageActions\";\n\n    setup() {\n        super.setup();\n        this.actionsMenuState = useDropdownState();\n        this.isMobileOS = isMobileOS;\n    }\n}\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Attachment[]} attachments\n * @property {function} unlinkAttachment\n * @property {number} imagesHeight\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 = { ImageActions };\n    static props = [\"attachments\", \"unlinkAttachment\", \"imagesHeight\", \"messageSearch?\"];\n    static template = \"mail.AttachmentList\";\n\n    setup() {\n        super.setup();\n        this.ui = useState(useService(\"ui\"));\n        // Arbitrary high value, this is effectively a max-width.\n        this.imagesWidth = 1920;\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            width: this.imagesWidth * 2,\n            height: this.props.imagesHeight * 2,\n        });\n    }\n\n    get images() {\n        return this.props.attachments.filter((a) => a.isImage);\n    }\n\n    get cards() {\n        return this.props.attachments.filter((a) => !a.isImage);\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        const downloadLink = document.createElement(\"a\");\n        downloadLink.setAttribute(\"href\", attachment.downloadUrl);\n        // Adding 'download' attribute into a link prevents open a new\n        // tab or change the current location of the window. This avoids\n        // interrupting the activity in the page such as rtc call.\n        downloadLink.setAttribute(\"download\", \"\");\n        downloadLink.click();\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.filename),\n            cancel: () => {},\n            confirm: () => this.onConfirmUnlink(attachment),\n        });\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: \"Remove\",\n                icon: \"fa fa-trash\",\n                onSelect: () => this.onClickUnlink(attachment),\n            });\n        }\n        if (this.canDownload(attachment)) {\n            res.push({\n                label: \"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", "import { Record } from \"@mail/core/common/record\";\nimport { assignDefined } from \"@mail/utils/common/misc\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { FileModelMixin } from \"@web/core/file_viewer/file_model\";\n\nexport class Attachment extends FileModelMixin(Record) {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").Attachment>} */\n    static records = {};\n    /** @returns {import(\"models\").Attachment} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Attachment|import(\"models\").Attachment[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n    static new() {\n        /** @type {import(\"models\").Attachment} */\n        const attachment = super.new(...arguments);\n        Record.onChange(attachment, [\"extension\", \"filename\"], () => {\n            if (!attachment.extension && attachment.filename) {\n                attachment.extension = attachment.filename.split(\".\").pop();\n            }\n        });\n        return attachment;\n    }\n\n    thread = Record.one(\"Thread\", { inverse: \"attachments\" });\n    res_name;\n    message = Record.one(\"Message\", { inverse: \"attachment_ids\" });\n    /** @type {luxon.DateTime} */\n    create_date = Record.attr(undefined, { type: \"datetime\" });\n\n    get isDeletable() {\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            const rpcParams = assignDefined(\n                { attachment_id: this.id },\n                { access_token: this.access_token }\n            );\n            const thread = this.thread || this.message?.thread;\n            if (thread) {\n                Object.assign(rpcParams, thread.rpcParams);\n            }\n            await rpc(\"/mail/attachment/delete\", rpcParams);\n        }\n        this.delete();\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.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                // FIXME: this should be only response. HOOT tests returns wrong data {result, error}\n                const attachmentData = response?.result ?? response;\n                this._processLoaded(thread, composer, attachmentData, 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 { Attachment } = this.store.insert(data);\n        const [attachment] = Attachment;\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.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            filename: upload.title,\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\nfunction 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    onWillUpdateProps,\n    onPatched,\n    onWillUnmount,\n    onMounted,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { hidePDFJSButtons } from \"@web/libs/pdfjs\";\n\n/**\n * @typedef {Object} Props\n * @property {number} threadId\n * @property {string} threadModel\n * @extends {Component<Props, Env>}\n */\nexport class AttachmentView extends Component {\n    static template = \"mail.AttachmentView\";\n    static components = {};\n    static props = [\"threadId\", \"threadModel\"];\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.uiService = useService(\"ui\");\n        this.mailPopoutService = useService(\"mail.popout\");\n        this.iframeViewerPdfRef = useRef(\"iframeViewerPdf\");\n        this.state = useState({\n            /** @type {import(\"models\").Thread|undefined} */\n            thread: undefined,\n        });\n        useEffect(() => {\n            if (this.iframeViewerPdfRef.el) {\n                hidePDFJSButtons(this.iframeViewerPdfRef.el);\n            }\n        });\n        this.updateFromProps(this.props);\n        onWillUpdateProps((props) => this.updateFromProps(props));\n\n        onMounted(this.updatePopout);\n        onPatched(this.updatePopout);\n        onWillUnmount(this.resetPopout);\n    }\n\n    onClickNext() {\n        const index = this.state.thread.attachmentsInWebClientView.findIndex((attachment) =>\n            attachment.eq(this.state.thread.mainAttachment)\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.mainAttachment)\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    popoutAttachment() {\n        this.mailPopoutService.addHooks(\n            () => {\n                // before popout hook\n                this.hide();\n                this.uiService.bus.trigger(\"resize\");\n            },\n            () => {\n                // after popout hook\n                this.show();\n                this.uiService.bus.trigger(\"resize\");\n            }\n        );\n        this.mailPopoutService.popout(PopoutAttachmentView, this.props);\n    }\n\n    get 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    show() {\n        const parentElementClassList = this.attachmentViewParentElementClassList;\n        const hiddenClass = \"d-none\";\n        if (parentElementClassList?.contains(hiddenClass)) {\n            parentElementClassList.remove(hiddenClass);\n        }\n    }\n\n    hide() {\n        const parentElementClassList = this.attachmentViewParentElementClassList;\n        const hiddenClass = \"d-none\";\n        if (!parentElementClassList?.contains(hiddenClass)) {\n            parentElementClassList.add(hiddenClass);\n        }\n    }\n\n    updatePopout() {\n        if (this.mailPopoutService.externalWindow) {\n            this.mailPopoutService.popout(PopoutAttachmentView, this.props);\n            this.hide();\n        }\n    }\n\n    resetPopout() {\n        this.mailPopoutService.reset();\n    }\n\n    get displayName() {\n        return this.state.thread.mainAttachment.filename;\n    }\n}\n\n/*\n * AttachmentView inside popout window.\n * Popout features disabled as this only makes sense in the non-popout AttachmentView.\n */\nclass PopoutAttachmentView extends AttachmentView {\n    static template = \"mail.PopoutAttachmentView\";\n    updatePopout() {}\n    resetPopout() {}\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        });\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                this.state.value = this.props.value;\n                this.inputRef.el.blur();\n                break;\n        }\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    /** @type {Object.<number, import(\"models\").CannedResponse>} */\n    static records = {};\n    /** @returns {import(\"models\").CannedResponse} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").CannedResponse|import(\"models\").CannedResponse[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    source;\n    /** @type {string} */\n    substitution;\n}\n\nCannedResponse.register();\n", "import { Store } from \"@mail/core/common/store_service\";\nimport { 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 id = \"id\";\n    /** @type {Object.<number, import(\"models\").ChannelMember>} */\n    static records = {};\n    /** @returns {import(\"models\").ChannelMember} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").ChannelMember|import(\"models\").ChannelMember[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @type {string} */\n    create_date;\n    /** @type {number} */\n    id;\n    /** @type {luxon.DateTime} */\n    last_interest_dt = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {luxon.DateTime} */\n    last_seen_dt = Record.attr(undefined, { type: \"datetime\" });\n    persona = Record.one(\"Persona\", { inverse: \"channelMembers\" });\n    thread = Record.one(\"Thread\", { inverse: \"channelMembers\" });\n    threadAsSelf = Record.one(\"Thread\", {\n        compute() {\n            if (this.store.self?.eq(this.persona)) {\n                return this.thread;\n            }\n        },\n    });\n    fetched_message_id = Record.one(\"Message\");\n    seen_message_id = Record.one(\"Message\");\n    syncUnread = true;\n    _syncUnread = Record.attr(false, {\n        compute() {\n            if (!this.syncUnread || !this.eq(this.thread?.selfMember)) {\n                return false;\n            }\n            return (\n                this.localNewMessageSeparator !== this.new_message_separator ||\n                this.localMessageUnreadCounter !== this.message_unread_counter\n            );\n        },\n        onUpdate() {\n            if (this._syncUnread) {\n                this.localNewMessageSeparator = this.new_message_separator;\n                this.localMessageUnreadCounter = this.message_unread_counter;\n            }\n        },\n    });\n    unreadSynced = Record.attr(true, {\n        compute() {\n            return this.localNewMessageSeparator === this.new_message_separator;\n        },\n        onUpdate() {\n            if (this.unreadSynced) {\n                this.hideUnreadBanner = false;\n            }\n        },\n    });\n    hideUnreadBanner = false;\n    localMessageUnreadCounter = 0;\n    localNewMessageSeparator = null;\n    message_unread_counter = 0;\n    message_unread_counter_bus_id = 0;\n    new_message_separator = null;\n    threadAsTyping = Record.one(\"Thread\", {\n        compute() {\n            return this.isTyping ? this.thread : undefined;\n        },\n        eager: true,\n        onAdd() {\n            browser.clearTimeout(this.typingTimeoutId);\n            this.typingTimeoutId = browser.setTimeout(\n                () => (this.isTyping = false),\n                Store.OTHER_LONG_TYPING\n            );\n        },\n        onDelete() {\n            browser.clearTimeout(this.typingTimeoutId);\n        },\n    });\n    /** @type {number} */\n    typingTimeoutId;\n\n    get name() {\n        return this.persona.name;\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    get totalUnreadMessageCounter() {\n        let counter = this.message_unread_counter;\n        if (!this.unreadSynced) {\n            counter += this.localMessageUnreadCounter;\n        }\n        return counter;\n    }\n}\n\nChannelMember.register();\n", "import { ImStatus } from \"@mail/core/common/im_status\";\n\nimport { Component, useEffect, useRef, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useHover, useMovable } from \"@mail/utils/common/hooks\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { CountryFlag } from \"@mail/core/common/country_flag\";\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class ChatBubble extends Component {\n    static components = { CountryFlag, ImStatus, Dropdown };\n    static props = [\"chatWindow\"];\n    static template = \"mail.ChatBubble\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.wasHover = false;\n        this.hover = useHover([\"root\", \"preview*\"], {\n            onHover: () => (this.preview.isOpen = true),\n            onHovering: [100, () => (this.state.showClose = true)],\n            onAway: () => {\n                this.state.showClose = false;\n                this.preview.isOpen = false;\n            },\n        });\n        this.preview = useDropdownState();\n        this.rootRef = useRef(\"root\");\n        this.state = useState({ bouncing: false, showClose: true });\n        useEffect(\n            () => {\n                this.state.bouncing = this.thread.importantCounter ? true : this.state.bouncing;\n            },\n            () => [this.thread.importantCounter]\n        );\n        if (this.env.embedLivechat) {\n            this.position = useState({ left: \"auto\", top: \"auto\" });\n            useMovable({\n                cursor: \"grabbing\",\n                ref: this.rootRef,\n                elements: \".o-mail-ChatBubble\",\n                onDrop: ({ top, left }) =>\n                    Object.assign(this.position, { left: `${left}px`, top: `${top}px` }),\n            });\n        }\n    }\n\n    /** @returns {import(\"models\").Thread} */\n    get thread() {\n        return this.props.chatWindow.thread;\n    }\n\n    get previewContent() {\n        const lastMessage = this.thread?.newestPersistentNotEmptyOfAllMessage;\n        if (!lastMessage) {\n            return false;\n        }\n        return lastMessage.inlineBody;\n    }\n}\n", "import { ChatWindow } from \"@mail/core/common/chat_window\";\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 { _t } from \"@web/core/l10n/translation\";\n\nexport class ChatHub extends Component {\n    static components = { 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 = useState(useService(\"mail.store\"));\n        this.ui = useState(useService(\"ui\"));\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.compactRef = useRef(\"compact\");\n        this.compactPosition = useState({ left: \"auto\", top: \"auto\" });\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            cursor: \"grabbing\",\n            ref: this.compactRef,\n            elements: \".o-mail-ChatHub-compact\",\n            onDrop: ({ top, left }) =>\n                Object.assign(this.compactPosition, { left: `${left}px`, top: `${top}px` }),\n        });\n    }\n\n    onResize() {\n        this.chatHub.onRecompute();\n    }\n\n    get chatSizeTransitionText() {\n        return this.chatHub.isBig ? _t(\"Make chats smaller\") : _t(\"Make chats bigger\");\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    toggleChatSize() {\n        this.chatHub.isBig = !this.chatHub.isBig;\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    get isShown() {\n        return !this.ui.isSmall;\n    }\n\n    expand() {\n        this.chatHub.compact = false;\n        Object.assign(this.compactPosition, { left: \"auto\", top: \"auto\" });\n        this.more.isOpen = this.chatHub.folded.length > this.chatHub.maxFolded;\n    }\n}\n\nregistry.category(\"main_components\").add(\"mail.ChatHub\", { Component: ChatHub });\n", "import { browser } from \"@web/core/browser/browser\";\nimport { Record } from \"./record\";\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 = 360; // same value as $o-mail-ChatWindow-width\n    WINDOW_LARGE = 510; // same value as $o-mail-ChatWindow-widthLarge\n\n    /** @returns {import(\"models\").ChatHub} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").ChatHub|import(\"models\").ChatHub[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n    isBig = Record.attr(false, {\n        compute() {\n            return browser.localStorage.getItem(\"mail.user_setting.chat_window_big\") === \"true\";\n        },\n        onUpdate() {\n            /** @this {import(\"models\").ChatHub} */\n            if (this.isBig) {\n                browser.localStorage.setItem(\n                    \"mail.user_setting.chat_window_big\",\n                    this.isBig.toString()\n                );\n            } else {\n                browser.localStorage.removeItem(\"mail.user_setting.chat_window_big\");\n            }\n        },\n    });\n    compact = false;\n    /** From left to right. Right-most will actually be folded */\n    opened = Record.many(\"ChatWindow\", {\n        inverse: \"hubAsOpened\",\n        /** @this {import(\"models\").ChatHub} */\n        onAdd(r) {\n            this.onRecompute();\n        },\n        /** @this {import(\"models\").ChatHub} */\n        onDelete() {\n            this.onRecompute();\n        },\n    });\n    /** From top to bottom. Bottom-most will actually be hidden */\n    folded = Record.many(\"ChatWindow\", {\n        inverse: \"hubAsFolded\",\n        /** @this {import(\"models\").ChatHub} */\n        onAdd(r) {\n            this.onRecompute();\n        },\n        /** @this {import(\"models\").ChatHub} */\n        onDelete() {\n            this.onRecompute();\n        },\n    });\n\n    closeAll() {\n        [...this.opened, ...this.folded].forEach((cw) => cw.close());\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    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(\n                available / ((this.isBig ? this.WINDOW_LARGE : this.WINDOW) + this.WINDOW_INBETWEEN)\n            )\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\nChatHub.register();\n", "import { 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 {\n    useHover,\n    useMessageEdition,\n    useMessageHighlight,\n    useMessageToReplyTo,\n} from \"@mail/utils/common/hooks\";\nimport { isEventHandled } from \"@web/core/utils/misc\";\n\nimport { Component, toRaw, useChildSubEnv, useRef, useState } from \"@odoo/owl\";\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\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\";\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        CountryFlag,\n        Dropdown,\n        DropdownItem,\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        this.store = useState(useService(\"mail.store\"));\n        this.messageEdition = useMessageEdition();\n        this.messageHighlight = useMessageHighlight();\n        this.messageToReplyTo = useMessageToReplyTo();\n        this.state = useState({\n            actionsDisabled: false,\n            actionsMenuOpened: false,\n            jumpThreadPresent: 0,\n            editingGuestName: false,\n            editingName: false,\n        });\n        this.ui = useState(useService(\"ui\"));\n        this.contentRef = useRef(\"content\");\n        this.threadActions = useThreadActions();\n        this.actionsMenuButtonHover = useHover(\"actionsMenuButton\");\n        this.parentChannelHover = useHover(\"parentChannel\");\n\n        useChildSubEnv({\n            closeActionPanel: () => this.threadActions.activeAction?.close(),\n            inChatWindow: true,\n            messageHighlight: this.messageHighlight,\n        });\n    }\n\n    get composerType() {\n        if (this.thread && this.thread.model !== \"discuss.channel\") {\n            return \"note\";\n        }\n        return undefined;\n    }\n\n    get thread() {\n        return this.props.chatWindow.thread;\n    }\n\n    get style() {\n        const maxHeight = !this.ui.isSmall ? \"max-height: 95vh;\" : \"\";\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; ${maxHeight}`;\n    }\n\n    onKeydown(ev) {\n        const chatWindow = toRaw(this.props.chatWindow);\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 (ev.key) {\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();\n                } else {\n                    this.store.chatHub.opened[index + 1].focus();\n                }\n                break;\n            }\n        }\n    }\n\n    onClickHeader() {\n        if (\n            this.ui.isSmall ||\n            this.state.editingName ||\n            !this.thread ||\n            this.state.actionsDisabled\n        ) {\n            return;\n        }\n        this.toggleFold();\n    }\n\n    toggleFold() {\n        const chatWindow = toRaw(this.props.chatWindow);\n        if (this.ui.isSmall || this.state.actionsMenuOpened) {\n            return;\n        }\n        chatWindow.fold();\n    }\n\n    async close(options) {\n        const chatWindow = toRaw(this.props.chatWindow);\n        await 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 { Record } from \"@mail/core/common/record\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\n/** @typedef {{ thread?: import(\"models\").Thread }} ChatWindowData */\n\nexport class ChatWindow extends Record {\n    static id = \"thread\";\n    /** @type {Object<number, import(\"models\").ChatWindow} */\n    static records = {};\n    /** @returns {import(\"models\").ChatWindow} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").ChatWindow|import(\"models\").ChatWindow[]} */\n    static insert() {\n        return super.insert(...arguments);\n    }\n\n    thread = Record.one(\"Thread\");\n    autofocus = 0;\n    hidden = false;\n    /** Whether the chat window was created from the messaging menu */\n    fromMessagingMenu = false;\n    hubAsOpened = Record.one(\"ChatHub\", {\n        /** @this {import(\"models\").ChatWindow} */\n        onAdd() {\n            this.hubAsFolded = undefined;\n        },\n        /** @this {import(\"models\").ChatWindow} */\n        onDelete() {\n            if (!this.thread && !this.hubAsOpened) {\n                this.delete();\n            }\n        },\n    });\n    hubAsFolded = Record.one(\"ChatHub\", {\n        /** @this {import(\"models\").ChatWindow} */\n        onAdd() {\n            this.hubAsOpened = undefined;\n        },\n    });\n\n    get displayName() {\n        return this.thread?.displayName ?? _t(\"New message\");\n    }\n\n    get isOpen() {\n        return Boolean(this.hubAsOpened);\n    }\n\n    async close(options = {}) {\n        const { escape = false } = options;\n        const chatHub = this.store.chatHub;\n        const indexAsOpened = chatHub.opened.findIndex((w) => w.eq(this));\n        const thread = this.thread;\n        if (thread) {\n            thread.state = \"closed\";\n        }\n        await this._onClose(options);\n        this.delete();\n        if (escape && indexAsOpened !== -1 && chatHub.opened.length > 0) {\n            chatHub.opened[indexAsOpened === 0 ? 0 : indexAsOpened - 1].focus();\n        }\n    }\n\n    focus() {\n        this.autofocus++;\n    }\n\n    fold() {\n        if (!this.thread) {\n            return this.close();\n        }\n        this.store.chatHub.folded.delete(this);\n        this.store.chatHub.folded.unshift(this);\n        this.thread.state = \"folded\";\n        this.notifyState();\n    }\n\n    open({ notifyState = true } = {}) {\n        this.store.chatHub.opened.delete(this);\n        this.store.chatHub.opened.unshift(this);\n        if (this.thread) {\n            this.thread.state = \"open\";\n            if (notifyState) {\n                this.notifyState();\n            }\n        }\n        this.focus();\n    }\n\n    notifyState() {\n        if (\n            this.store.env.services.ui.isSmall ||\n            this.thread?.isTransient ||\n            !this.thread?.hasSelfAsMember\n        ) {\n            return;\n        }\n        if (this.thread?.model === \"discuss.channel\") {\n            this.thread.foldStateCount++;\n            return rpc(\n                \"/discuss/channel/fold\",\n                {\n                    channel_id: this.thread.id,\n                    state: this.thread.state,\n                    state_count: this.thread.foldStateCount,\n                },\n                { shadow: true }\n            );\n        }\n    }\n\n    async _onClose({ notifyState = true } = {}) {\n        if (notifyState) {\n            this.notifyState();\n        }\n    }\n}\n\nChatWindow.register();\n", "import { AttachmentList } from \"@mail/core/common/attachment_list\";\nimport { useAttachmentUploader } from \"@mail/core/common/attachment_uploader_hook\";\nimport { useDropzone } from \"@web/core/dropzone/dropzone_hook\";\nimport { Picker, usePicker } from \"@mail/core/common/picker\";\nimport { MessageConfirmDialog } from \"@mail/core/common/message_confirm_dialog\";\nimport { NavigableList } from \"@mail/core/common/navigable_list\";\nimport { useSuggestion } from \"@mail/core/common/suggestion_hook\";\nimport { prettifyMessageContent } from \"@mail/utils/common/format\";\nimport { useSelection } from \"@mail/utils/common/hooks\";\nimport { isDragSourceExternalFile } from \"@mail/utils/common/misc\";\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    useChildSubEnv,\n    useEffect,\n    useRef,\n    useState,\n    useExternalListener,\n    toRaw,\n} from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\nimport { escape, sprintf } from \"@web/core/utils/strings\";\nimport { isDisplayStandalone, isIOS, isMobileOS } from \"@web/core/browser/feature_detection\";\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 {import(\"@mail/utils/common/hooks\").MessageToReplyTo} messageToReplyTo\n * @property {import(\"@mail/utils/common/hooks\").MessageEdition} [messageEdition]\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        AttachmentList,\n        Picker,\n        FileUploader,\n        NavigableList,\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        \"messageToReplyTo?\",\n        \"onCloseFullComposerCallback?\",\n        \"onDiscardCallback?\",\n        \"onPostCallback?\",\n        \"mode?\",\n        \"placeholder?\",\n        \"dropzoneRef?\",\n        \"messageEdition?\",\n        \"className?\",\n        \"sidebar?\",\n        \"type?\",\n        \"showFullComposer?\",\n        \"allowUpload?\",\n    ];\n    static template = \"mail.Composer\";\n\n    setup() {\n        super.setup();\n        this.isMobileOS = isMobileOS();\n        this.isIosPwa = isIOS() && isDisplayStandalone();\n        this.OR_PRESS_SEND_KEYBIND = markup(\n            _t(\"or press %(send_keybind)s\", {\n                send_keybind: this.sendKeybinds\n                    .map((key) => `<samp>${escape(key)}</samp>`)\n                    .join(\" + \"),\n            })\n        );\n        this.store = useState(useService(\"mail.store\"));\n        this.attachmentUploader = useAttachmentUploader(\n            this.thread ?? this.props.composer.message.thread,\n            { composer: this.props.composer }\n        );\n        this.ui = useState(useService(\"ui\"));\n        this.mainActionsRef = useRef(\"main-actions\");\n        this.ref = useRef(\"textarea\");\n        this.fakeTextarea = useRef(\"fakeTextarea\");\n        this.emojiButton = useRef(\"emoji-button\");\n        this.inputContainerRef = useRef(\"input-container\");\n        this.state = useState({\n            active: true,\n        });\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                );\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        useExternalListener(window, \"beforeunload\", this.saveContent.bind(this));\n        if (this.props.dropzoneRef) {\n            useDropzone(\n                this.props.dropzoneRef,\n                this.onDropFile,\n                \"o-mail-Composer-dropzone\",\n                () => this.allowUpload\n            );\n        }\n        if (this.props.messageEdition) {\n            this.props.messageEdition.composerOfThread = this;\n        }\n        useChildSubEnv({\n            inComposer: true,\n        });\n        this.picker = usePicker(this.pickerSettings);\n        useEffect(\n            (focus) => {\n                if (focus && this.ref.el) {\n                    this.selection.restore();\n                    this.ref.el.focus();\n                }\n            },\n            () => [this.props.autofocus + this.props.composer.autofocus, this.props.placeholder]\n        );\n        useEffect(\n            (rThread, cThread) => {\n                if (cThread && cThread.eq(rThread)) {\n                    this.props.composer.autofocus++;\n                }\n            },\n            () => [this.props.messageToReplyTo?.thread, this.props.composer.thread]\n        );\n        useEffect(\n            () => {\n                if (this.fakeTextarea.el.scrollHeight) {\n                    this.ref.el.style.height = this.fakeTextarea.el.scrollHeight + \"px\";\n                }\n                this.saveContentDebounced();\n            },\n            () => [this.props.composer.text, 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.text) {\n                this.restoreContent();\n            }\n        });\n    }\n\n    get pickerSettings() {\n        return {\n            anchor: this.props.mode === \"extended\" ? undefined : this.mainActionsRef,\n            buttons: [this.emojiButton],\n            close: () => {\n                if (!this.ui.isSmall) {\n                    this.props.composer.autofocus++;\n                }\n            },\n            pickers: { emoji: (emoji) => this.addEmoji(emoji) },\n            position:\n                this.props.mode === \"extended\"\n                    ? \"bottom-start\"\n                    : this.props.composer.message\n                    ? \"bottom-start\"\n                    : \"top-end\",\n            fixed: !this.props.composer.message,\n        };\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    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        if (this.ui.isSmall) {\n            return markup(\n                sprintf(\n                    escape(\n                        _t(\n                            \"%(open_button)s%(icon)s%(open_em)sDiscard editing%(close_em)s%(close_button)s\"\n                        )\n                    ),\n                    {\n                        open_button: `<button class='btn px-1 py-0' data-type=\"${escape(\n                            EDIT_CLICK_TYPE.CANCEL\n                        )}\">`,\n                        close_button: \"</button>\",\n                        icon: `<i class='fa fa-times-circle pe-1' data-type=\"${escape(\n                            EDIT_CLICK_TYPE.CANCEL\n                        )}\"></i>`,\n                        open_em: `<em data-type=\"${escape(EDIT_CLICK_TYPE.CANCEL)}\">`,\n                        close_em: \"</em>\",\n                    }\n                )\n            );\n        } else {\n            const translation1 = _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            );\n            const translation2 = _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            );\n            return markup(\n                sprintf(escape(this.props.mode === \"extended\" ? translation1 : translation2), {\n                    open_samp: \"<samp>\",\n                    close_samp: \"</samp>\",\n                    open_em: \"<em>\",\n                    close_em: \"</em>\",\n                    open_cancel: `<a role=\"button\" href=\"#\" data-type=\"${escape(\n                        EDIT_CLICK_TYPE.CANCEL\n                    )}\">`,\n                    close_cancel: \"</a>\",\n                    open_save: `<a role=\"button\" href=\"#\" data-type=\"${escape(\n                        EDIT_CLICK_TYPE.SAVE\n                    )}\">`,\n                    close_save: \"</a>\",\n                })\n            );\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.props.mode === \"extended\" ? [_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.messageToReplyTo?.message?.thread ?? this.props.composer.thread ?? null;\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            (!this.props.composer.text && attachments.length === 0) ||\n            attachments.some(({ uploading }) => Boolean(uploading))\n        );\n    }\n\n    get hasSendButtonNonEditing() {\n        return !this.extended;\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 {\n                            return {\n                                label: suggestion.name,\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                        return {\n                            label: suggestion.parent_channel_id\n                                ? `${suggestion.parent_channel_id.displayName} > ${suggestion.displayName}`\n                                : suggestion.displayName,\n                            thread: suggestion,\n                            classList: \"o-mail-Composer-suggestion\",\n                        };\n                    }),\n                };\n            case \"ChannelCommand\":\n                return {\n                    ...props,\n                    optionTemplate: \"mail.Composer.suggestionChannelCommand\",\n                    options: suggestions.map((suggestion) => {\n                        return {\n                            label: suggestion.name,\n                            help: suggestion.help,\n                            classList: \"o-mail-Composer-suggestion\",\n                        };\n                    }),\n                };\n            case \"mail.canned.response\":\n                return {\n                    ...props,\n                    autoSelectFirst: false,\n                    hint: _t(\"Tab to select\"),\n                    optionTemplate: \"mail.Composer.suggestionCannedResponse\",\n                    options: suggestions.map((suggestion) => {\n                        return {\n                            cannedResponse: suggestion,\n                            source: suggestion.source,\n                            label: suggestion.substitution,\n                            classList: \"o-mail-Composer-suggestion\",\n                        };\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() {\n        if (this.props.onCloseFullComposerCallback) {\n            this.props.onCloseFullComposerCallback();\n        } else {\n            this.thread?.fetchNewMessages();\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.props.messageEdition && composer.text === \"\") {\n                    const messageToEdit = composer.thread.lastEditableMessageOfSelf;\n                    if (messageToEdit) {\n                        this.props.messageEdition.editingMessage = messageToEdit;\n                    }\n                }\n                break;\n            case \"Enter\": {\n                if (isEventHandled(ev, \"NavigableList.select\") || !this.state.active) {\n                    ev.preventDefault();\n                    return;\n                }\n                const shouldPost = this.props.mode === \"extended\" ? 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    onClickAddAttachment(ev) {\n        const composer = toRaw(this.props.composer);\n        markEventHandled(ev, \"composer.clickOnAddAttachment\");\n        composer.autofocus++;\n    }\n\n    async onClickFullComposer(ev) {\n        if (this.props.type !== \"note\") {\n            // auto-create partners of checked suggested partners\n            const newPartners = this.thread.suggestedRecipients.filter(\n                (recipient) => recipient.checked && !recipient.persona\n            );\n            if (newPartners.length !== 0) {\n                const recipientEmails = [];\n                const recipientAdditionalValues = {};\n                newPartners.forEach((recipient) => {\n                    recipientEmails.push(recipient.email);\n                    recipientAdditionalValues[recipient.email] = recipient.create_values || {};\n                });\n                const partners = await rpc(\"/mail/partner/from_email\", {\n                    emails: recipientEmails,\n                    additional_values: recipientAdditionalValues,\n                });\n                for (const index in partners) {\n                    const partnerData = partners[index];\n                    const persona = this.store.Persona.insert({ ...partnerData, type: \"partner\" });\n                    const email = recipientEmails[index];\n                    const recipient = this.thread.suggestedRecipients.find(\n                        (recipient) => recipient.email === email\n                    );\n                    Object.assign(recipient, { persona });\n                }\n            }\n        }\n        const attachmentIds = this.props.composer.attachments.map((attachment) => attachment.id);\n        const body = this.props.composer.text;\n        const validMentions = this.store.getMentionsFromText(body, {\n            mentionedChannels: this.props.composer.mentionedChannels,\n            mentionedPartners: this.props.composer.mentionedPartners,\n        });\n        const context = {\n            default_attachment_ids: attachmentIds,\n            default_body: await prettifyMessageContent(body, validMentions),\n            default_model: this.thread.model,\n            default_partner_ids:\n                this.props.type === \"note\"\n                    ? []\n                    : this.thread.suggestedRecipients\n                          .filter((recipient) => recipient.checked)\n                          .map((recipient) => recipient.persona.id),\n            default_res_ids: [this.thread.id],\n            default_subtype_xmlid: this.props.type === \"note\" ? \"mail.mt_note\" : \"mail.mt_comment\",\n            mail_post_autofollow: this.thread.hasWriteAccess,\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 === [] : click on 'X' or press escape\n                // args === { special: true } : click on 'discard'\n                const accidentalDiscard = args.length === 0;\n                const isDiscard = accidentalDiscard || args[0]?.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                    const editor = document.querySelector(\n                        \".o_mail_composer_form_view .note-editable\"\n                    );\n                    const editorIsEmpty = !editor || !editor.innerText.replace(/^\\s*$/gm, \"\");\n                    if (!editorIsEmpty) {\n                        this.saveContent();\n                        this.restoreContent();\n                    }\n                } else {\n                    this.clear();\n                }\n                this.props.messageToReplyTo?.cancel();\n                this.onCloseFullComposerCallback();\n            },\n        };\n        await this.env.services.action.doAction(action, options);\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    onClickAddEmoji(ev) {\n        markEventHandled(ev, \"Composer.onClickAddEmoji\");\n    }\n\n    isEventTrusted(ev) {\n        // Allow patching during tests\n        return ev.isTrusted;\n    }\n\n    async processMessage(cb) {\n        const el = this.ref.el;\n        const attachments = this.props.composer.attachments;\n        if (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 (\n            this.props.composer.text.trim() ||\n            attachments.length > 0 ||\n            (this.message && this.message.attachment_ids.length > 0)\n        ) {\n            if (!this.state.active) {\n                return;\n            }\n            this.state.active = false;\n            await cb(this.props.composer.text);\n            if (this.props.onPostCallback) {\n                this.props.onPostCallback();\n            }\n            this.clear();\n            this.state.active = true;\n            el.focus();\n        }\n    }\n\n    async sendMessage() {\n        const composer = toRaw(this.props.composer);\n        if (composer.message) {\n            this.editMessage();\n            return;\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            isNote: this.props.type === \"note\",\n            mentionedChannels: composer.mentionedChannels || [],\n            mentionedPartners: composer.mentionedPartners || [],\n            cannedResponseIds: composer.cannedResponses.map((c) => c.id),\n            parentId: this.props.messageToReplyTo?.message?.id,\n        };\n    }\n\n    /**\n     * @typedef postData\n     * @property {import('@mail/attachments/attachment_model').Attachment[]} attachments\n     * @property {boolean} isNote\n     * @property {number} parentId\n     * @property {integer[]} mentionedChannelIds\n     * @property {integer[]} mentionedPartnerIds\n     */\n\n    /**\n     * @param {string} 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        if (postThread.model === \"discuss.channel\") {\n            // feature of (optimistic) temp message\n            post();\n        } else {\n            await post();\n        }\n        if (thread.model === \"mail.box\") {\n            this.notifySendFromMailbox();\n        }\n        this.suggestion?.clearRawMentions();\n        this.suggestion?.clearCannedResponses();\n        this.props.messageToReplyTo?.cancel();\n    }\n\n    async editMessage() {\n        const composer = toRaw(this.props.composer);\n        if (composer.text || composer.message.attachment_ids.length > 0) {\n            await this.processMessage(async (value) =>\n                composer.message.edit(value, composer.attachments, {\n                    mentionedChannels: composer.mentionedChannels,\n                    mentionedPartners: composer.mentionedPartners,\n                })\n            );\n        } else {\n            this.env.services.dialog.add(MessageConfirmDialog, {\n                message: composer.message,\n                onConfirm: () => this.message.remove(),\n                prompt: _t(\"Are you sure you want to delete this message?\"),\n            });\n        }\n        this.suggestion?.clearRawMentions();\n    }\n\n    addEmoji(str) {\n        const composer = toRaw(this.props.composer);\n        const text = composer.text;\n        const firstPart = text.slice(0, composer.selection.start);\n        const secondPart = text.slice(composer.selection.end, text.length);\n        composer.text = firstPart + str + secondPart;\n        this.selection.moveCursor((firstPart + str).length);\n        if (!this.ui.isSmall) {\n            composer.autofocus++;\n        }\n    }\n\n    onFocusin() {\n        const composer = toRaw(this.props.composer);\n        composer.isFocused = true;\n        composer.thread?.markAsRead();\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 fullComposerContent =\n            document\n                .querySelector(\".o_mail_composer_form_view .note-editable\")\n                ?.innerText.replace(/(\\t|\\n)+/g, \"\\n\") ?? composer.text;\n        browser.localStorage.setItem(composer.localId, fullComposerContent);\n    }\n\n    restoreContent() {\n        const composer = toRaw(this.props.composer);\n        const fullComposerContent = browser.localStorage.getItem(composer.localId);\n        if (fullComposerContent) {\n            composer.text = fullComposerContent;\n        }\n    }\n}\n", "import { OR, Record } from \"@mail/core/common/record\";\n\nexport class Composer extends Record {\n    static id = OR(\"thread\", \"message\");\n    /** @returns {import(\"models\").Composer} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Composer|import(\"models\").Composer[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    clear() {\n        this.attachments.length = 0;\n        this.text = \"\";\n        Object.assign(this.selection, {\n            start: 0,\n            end: 0,\n            direction: \"none\",\n        });\n    }\n\n    attachments = Record.many(\"Attachment\");\n    message = Record.one(\"Message\");\n    mentionedPartners = Record.many(\"Persona\");\n    mentionedChannels = Record.many(\"Thread\");\n    cannedResponses = Record.many(\"mail.canned.response\");\n    text = \"\";\n    thread = Record.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 = false;\n    autofocus = 0;\n}\n\nComposer.register();\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    /** @type {Object.<number, import(\"models\").Country>} */\n    static records = {};\n    /** @returns {import(\"models\").Country} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Country|import(\"models\").Country[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n    /** @type {string} */\n    code;\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    name;\n\n    get flagUrl() {\n        return `/base/static/img/country_flags/${encodeURIComponent(this.code.toLowerCase())}.png`;\n    }\n}\n\nCountry.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 { registry } from \"@web/core/registry\";\n\nexport const discussComponentRegistry = registry.category(\"discuss.component\");\n", "import { Component, xml } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { EMOJI_PICKER_PROPS, EmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\n\nexport class EmojiPickerMobile extends Component {\n    static components = { Dialog, EmojiPicker };\n    static props = EMOJI_PICKER_PROPS;\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'\">\n            <EmojiPicker t-props=\"props\"/>\n        </Dialog>\n    `;\n}\n", "import { 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    /** @type {Object.<number, import(\"models\").Failure>} */\n    static records = {};\n    /** @returns {import(\"models\").Failure} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Failure|import(\"models\").Failure[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    notifications = Record.many(\"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]?.message?.thread?.modelName;\n    }\n    get resModel() {\n        return this.notifications?.[0]?.message?.thread?.model;\n    }\n    get resIds() {\n        return new Set([\n            ...this.notifications.map((notif) => notif.message?.thread?.id).filter((id) => !!id),\n        ]);\n    }\n    lastMessage = Record.one(\"Message\", {\n        /** @this {import(\"models\").Failure} */\n        compute() {\n            let lastMsg = this.notifications[0]?.message;\n            for (const notification of this.notifications) {\n                if (lastMsg?.id < notification.message?.id) {\n                    lastMsg = notification.message;\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.jpg\";\n    }\n\n    get body() {\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 { Record } from \"@mail/core/common/record\";\n\nexport class Follower extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").Follower>} */\n    static records = {};\n    /** @returns {import(\"models\").Follower} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Follower|import(\"models\").Follower[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    thread = Record.one(\"Thread\");\n    /** @type {number} */\n    id;\n    /** @type {boolean} */\n    is_active;\n    partner = Record.one(\"Persona\");\n\n    /** @returns {boolean} */\n    get isEditable() {\n        const hasWriteAccess = this.thread ? this.thread.hasWriteAccess : false;\n        return this.partner.eq(this.store.self) ? this.thread.hasReadAccess : hasWriteAccess;\n    }\n\n    async remove() {\n        await this.store.env.services.orm.call(this.thread.model, \"message_unsubscribe\", [\n            [this.thread.id],\n            [this.partner.id],\n        ]);\n        this.delete();\n    }\n\n    removeRecipient() {\n        this.thread.recipients.delete(this);\n    }\n}\n\nFollower.register();\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?\", \"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", "/* @odoo-module */\n\nimport { AWAY_DELAY, imStatusService } from \"@bus/im_status_service\";\nimport { patch } from \"@web/core/utils/patch\";\n\nexport const imStatusServicePatch = {\n    start(env, services) {\n        const { bus_service, presence } = services;\n        const API = super.start(env, services);\n\n        bus_service.subscribe(\n            \"bus.bus/im_status_updated\",\n            ({ im_status, partner_id, guest_id }) => {\n                const store = env.services[\"mail.store\"];\n                if (!store) {\n                    return;\n                }\n                const persona = store.Persona.get({\n                    type: partner_id ? \"partner\" : \"guest\",\n                    id: partner_id ?? guest_id,\n                });\n                if (!persona) {\n                    return; // Do not store unknown persona's status\n                }\n                persona.im_status = im_status;\n                if (persona.type !== \"guest\" || persona.notEq(store.self)) {\n                    return; // Partners are already handled by the original service\n                }\n                const isOnline = presence.getInactivityPeriod() < AWAY_DELAY;\n                if ((im_status === \"away\" && isOnline) || im_status === \"offline\") {\n                    this.updateBusPresence();\n                }\n            }\n        );\n        return API;\n    },\n};\nexport const unpatchImStatusService = patch(imStatusService, imStatusServicePatch);\n", "import { LinkPreviewConfirmDelete } from \"@mail/core/common/link_preview_confirm_delete\";\n\nimport { Component } 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 {boolean} [deletable]\n * @extends {Component<Props, Env>}\n */\nexport class LinkPreview extends Component {\n    static template = \"mail.LinkPreview\";\n    static props = [\"linkPreview\", \"deletable\"];\n    static components = {};\n\n    setup() {\n        super.setup();\n        this.dialogService = useService(\"dialog\");\n    }\n\n    onClick() {\n        this.dialogService.add(LinkPreviewConfirmDelete, {\n            linkPreview: this.props.linkPreview,\n            LinkPreview,\n        });\n    }\n\n    onImageLoaded() {\n        this.env.onImageLoaded?.();\n    }\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { Component, useState } 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} close\n * @property {Component} LinkPreviewListComponent\n * @extends {Component<Props, Env>}\n */\nexport class LinkPreviewConfirmDelete extends Component {\n    static components = { Dialog };\n    static props = [\"linkPreview\", \"close\", \"LinkPreview\"];\n    static template = \"mail.LinkPreviewConfirmDelete\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n    }\n\n    get message() {\n        return this.props.linkPreview.message;\n    }\n\n    onClickOk() {\n        rpc(\n            \"/mail/link_preview/hide\",\n            { link_preview_ids: [this.props.linkPreview.id] },\n            { silent: true }\n        );\n        this.props.close();\n    }\n\n    onClickDeleteAll() {\n        rpc(\n            \"/mail/link_preview/hide\",\n            { link_preview_ids: this.message.linkPreviews.map((lp) => lp.id) },\n            { silent: true }\n        );\n        this.props.close();\n    }\n\n    onClickCancel() {\n        this.props.close();\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\").LinkPreview[]} linkPreviews\n * @property {boolean} [deletable]\n * @extends {Component<Props, Env>}\n */\nexport class LinkPreviewList extends Component {\n    static template = \"mail.LinkPreviewList\";\n    static props = [\"linkPreviews\", \"deletable?\"];\n    static defaultProps = {\n        deletable: false,\n    };\n    static components = { LinkPreview };\n}\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class LinkPreview extends Record {\n    static id = \"id\";\n    /** @returns {import(\"models\").LinkPreview} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").LinkPreview|import(\"models\").LinkPreview[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @type {number} */\n    id;\n    message = Record.one(\"Message\", { inverse: \"linkPreviews\" });\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 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        return Boolean(!this.isImage && this.og_type && this.og_type.startsWith(\"video\"));\n    }\n\n    get isCard() {\n        return !this.isImage && !this.isVideo;\n    }\n}\n\nLinkPreview.register();\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 {Partial<import(\"services\").Services>} 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.Message.insert(messageData);\n            }\n            const attachment = this.store.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.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, { html: true });\n        });\n    }\n\n    _handleNotificationToggleStar(payload, metadata) {\n        const { message_ids: messageIds, starred } = payload;\n        this.store.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 {Partial<import(\"services\").Services>} 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 { registry } from \"@web/core/registry\";\nimport { App } from \"@odoo/owl\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport const mailPopoutService = {\n    start(env) {\n        let externalWindow;\n        let beforeFn;\n        let afterFn;\n        let app;\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         */\n        function reset() {\n            if (externalWindow) {\n                externalWindow.document.head.innerHTML = \"\";\n                externalWindow.document.write(window.document.head.outerHTML);\n                externalWindow.document.body = externalWindow.document.createElement(\"body\");\n            }\n            if (app) {\n                app.destroy();\n                app = null;\n            }\n        }\n\n        /**\n         * Poll the external window to detect when it is closed.\n         * the afterPopout hook (afterFn) is then called after the window is closed\n         */\n        async function pollClosedWindow() {\n            while (externalWindow) {\n                await new Promise((r) => setTimeout(r, 1000));\n                if (externalWindow.closed) {\n                    externalWindow = null;\n                    afterFn();\n                }\n            }\n        }\n\n        /**\n         * This function registers hooks (before/after the window popout)\n         * @param {Function} beforePopout: this function is called before the component is initially mounted on the external window.\n         * @param {Function} afterPopout: this function is called after the external window is closed.\n         */\n        function addHooks(beforePopout = () => {}, afterPopout = () => {}) {\n            beforeFn = beforePopout;\n            afterFn = afterPopout;\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         * @param {class} component: The component to be mounted.\n         * @param {Props} props: The props of the component.\n         * @returns {Window} The external window\n         */\n        function popout(component, props) {\n            if (!externalWindow || externalWindow.closed) {\n                externalWindow = browser.open(\"about:blank\", \"_blank\", \"popup=yes\");\n                window.addEventListener(\"beforeunload\", () => {\n                    if (externalWindow && !externalWindow.closed) {\n                        externalWindow.close();\n                    }\n                });\n                pollClosedWindow();\n            }\n\n            beforeFn();\n            reset();\n            app = new App(component, {\n                name: \"Popout\",\n                env,\n                props,\n                getTemplate,\n            });\n            app.mount(externalWindow.document.body);\n            return externalWindow;\n        }\n\n        return {\n            get externalWindow() {\n                return externalWindow && externalWindow.closed ? null : externalWindow;\n            },\n            popout,\n            reset,\n            addHooks,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"mail.popout\", mailPopoutService);\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 { LinkPreviewList } from \"@mail/core/common/link_preview_list\";\nimport { MessageInReply } from \"@mail/core/common/message_in_reply\";\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 { MessageSeenIndicator } from \"@mail/core/common/message_seen_indicator\";\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    markup,\n    onMounted,\n    onPatched,\n    onWillDestroy,\n    onWillUpdateProps,\n    toRaw,\n    useChildSubEnv,\n    useEffect,\n    useRef,\n    useState,\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 { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { url } from \"@web/core/utils/urls\";\nimport { messageActionsRegistry, useMessageActions } from \"./message_actions\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { MessageActionMenuMobile } from \"./message_action_menu_mobile\";\nimport { discussComponentRegistry } from \"./discuss_component_registry\";\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 {import(\"@mail/utils/common/hooks\").MessageToReplyTo} [messageToReplyTo]\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        ActionSwiper,\n        AttachmentList,\n        Composer,\n        Dropdown,\n        DropdownItem,\n        LinkPreviewList,\n        MessageInReply,\n        MessageReactions,\n        MessageSeenIndicator,\n        ImStatus,\n        Popover: MessageNotificationPopover,\n        RelativeTime,\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        \"messageEdition?\",\n        \"messageToReplyTo?\",\n        \"previousMessage?\",\n        \"squashed?\",\n        \"thread?\",\n        \"messageSearch?\",\n        \"className?\",\n        \"showDates?\",\n        \"isFirstMessage?\",\n    ];\n    static template = \"mail.Message\";\n\n    setup() {\n        super.setup();\n        this.escape = escape;\n        this.popover = usePopover(this.constructor.components.Popover, { position: \"top\" });\n        this.state = useState({\n            isEditing: false,\n            isHovered: false,\n            isClicked: false,\n            expandOptions: false,\n            emailHeaderOpen: false,\n            showTranslation: false,\n            actionMenuMobileOpen: false,\n        });\n        /** @type {ShadowRoot} */\n        this.shadowRoot;\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.hasTouch = hasTouch;\n        this.messageBody = useRef(\"body\");\n        this.messageActions = useMessageActions();\n        this.store = useState(useService(\"mail.store\"));\n        this.shadowBody = useRef(\"shadowBody\");\n        this.dialog = useService(\"dialog\");\n        this.ui = useState(useService(\"ui\"));\n        this.openReactionMenu = this.openReactionMenu.bind(this);\n        this.optionsDropdown = useDropdownState();\n        useChildSubEnv({\n            message: this.props.message,\n            alignedRight: this.isAlignedRight,\n        });\n        useEffect(\n            (editingMessage) => {\n                if (this.props.message.eq(editingMessage)) {\n                    messageActionsRegistry.get(\"edit\").onClick(this);\n                }\n            },\n            () => [this.props.messageEdition?.editingMessage]\n        );\n        onMounted(() => {\n            if (this.shadowBody.el) {\n                this.shadowRoot = this.shadowBody.el.attachShadow({ mode: \"open\" });\n                const color = cookie.get(\"color_scheme\") === \"dark\" ? \"white\" : \"black\";\n                const shadowStyle = document.createElement(\"style\");\n                shadowStyle.innerHTML = `\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 (cookie.get(\"color_scheme\") === \"dark\") {\n                    this.shadowRoot.appendChild(shadowStyle);\n                }\n            }\n        });\n        useEffect(\n            () => {\n                if (this.messageBody.el) {\n                    this.prepareMessageBody(this.messageBody.el);\n                }\n                if (this.shadowBody.el) {\n                    const bodyEl = document.createElement(\"span\");\n                    bodyEl.innerHTML = this.state.showTranslation\n                        ? this.message.translationValue\n                        : this.props.messageSearch?.highlight(this.message.body) ??\n                          this.message.body;\n                    this.prepareMessageBody(bodyEl);\n                    this.shadowRoot.appendChild(bodyEl);\n                    return () => {\n                        this.shadowRoot.removeChild(bodyEl);\n                    };\n                }\n            },\n            () => [\n                this.state.showTranslation,\n                this.message.translationValue,\n                this.props.messageSearch?.searchTerm,\n                this.message.body,\n            ]\n        );\n    }\n\n    get attClass() {\n        return {\n            [this.props.className]: true,\n            \"o-card p-2 mt-2\": this.props.asCard,\n            \"pt-1\": !this.props.asCard,\n            \"o-selfAuthored\": this.message.isSelfAuthored && !this.env.messageCard,\n            \"o-selected\": this.props.messageToReplyTo?.isSelected(\n                this.props.thread,\n                this.props.message\n            ),\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-2\": this.props.isInChatWindow,\n            \"opacity-50\": this.props.messageToReplyTo?.isNotSelected(\n                this.props.thread,\n                this.props.message\n            ),\n            \"o-actionMenuMobileOpen\": this.state.actionMenuMobileOpen,\n            \"o-editing\": this.state.isEditing,\n        };\n    }\n\n    get authorAvatarAttClass() {\n        return {\n            o_object_fit_contain: this.props.message.author?.is_company,\n            o_object_fit_cover: !this.props.message.author?.is_company,\n        };\n    }\n\n    get authorName() {\n        if (this.message.author) {\n            return this.message.author.name;\n        }\n        return this.message.email_from;\n    }\n\n    get authorAvatarUrl() {\n        if (\n            this.message.message_type &&\n            this.message.message_type.includes(\"email\") &&\n            ![\"partner\", \"guest\"].includes(this.message.author?.type)\n        ) {\n            return url(\"/mail/static/src/img/email_icon.png\");\n        }\n\n        if (this.message.author) {\n            return this.message.author.avatarUrl;\n        }\n\n        return this.store.DEFAULT_AVATAR;\n    }\n\n    get expandText() {\n        return _t(\"Expand\");\n    }\n\n    get message() {\n        return this.props.message;\n    }\n\n    /** Max amount of quick actions, including \"...\" */\n    get quickActionCount() {\n        return this.env.inChatter ? 3 : this.env.inChatWindow ? 2 : 4;\n    }\n\n    get showSeenIndicator() {\n        return this.props.message.isSelfAuthored && this.props.thread?.hasSeenFeature;\n    }\n\n    get showSubtypeDescription() {\n        return (\n            this.message.subtype_description &&\n            this.message.subtype_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 (\n            !this.props.message.is_discussion &&\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            this.optionsDropdown.isOpen\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 !this.isOriginThread && !this.message.is_transient && this.message.thread;\n    }\n\n    get isOriginThread() {\n        if (!this.props.thread) {\n            return false;\n        }\n        return this.props.thread.eq(this.message.thread);\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    /**\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.Message.insert({\n                    id: Number(oeId),\n                    res_id: this.props.thread.id,\n                    model: this.props.thread.model,\n                }),\n                this.props.thread\n            );\n        }\n    }\n\n    /** @param {HTMLElement} bodyEl */\n    prepareMessageBody(bodyEl) {\n        if (!bodyEl) {\n            return;\n        }\n        const linkEls = bodyEl.querySelectorAll(\".o_channel_redirect\");\n        for (const linkEl of linkEls) {\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    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        const message = toRaw(this.props.message);\n        this.props.messageEdition?.exitEditMode();\n        message.composer = undefined;\n        this.state.isEditing = false;\n    }\n\n    onClickNotification(ev) {\n        const message = toRaw(this.message);\n        if (message.failureNotifications.length > 0) {\n            this.onClickFailure(ev);\n        } else {\n            this.popover.open(ev.target, { message });\n        }\n    }\n\n    onClickFailure(ev) {\n        const message = toRaw(this.message);\n        markEventHandled(ev, \"Message.ClickFailure\");\n        this.env.services.action.doAction(\"mail.mail_resend_message_action\", {\n            additionalContext: {\n                mail_message_to_resend: message.id,\n            },\n        });\n    }\n\n    /** @param {MouseEvent} [ev] */\n    openMobileActions(ev) {\n        if (!isMobileOS()) {\n            return;\n        }\n        ev?.stopPropagation();\n        this.state.actionMenuMobileOpen = true;\n        this.dialog.add(\n            MessageActionMenuMobile,\n            {\n                message: this.props.message,\n                thread: this.props.thread,\n                isFirstMessage: this.props.isFirstMessage,\n                messageToReplyTo: this.props.messageToReplyTo,\n                openReactionMenu: () => this.openReactionMenu(),\n                state: this.state,\n            },\n            { context: this, onClose: () => (this.state.actionMenuMobileOpen = false) }\n        );\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        const message = toRaw(this.message);\n        if (!message.translationValue) {\n            const { error, lang_name, body } = await rpc(\"/mail/message/translate\", {\n                message_id: message.id,\n            });\n            message.translationValue = body && markup(body);\n            message.translationSource = lang_name;\n            message.translationErrors = error;\n        }\n        this.state.showTranslation =\n            !this.state.showTranslation && Boolean(message.translationValue);\n    }\n}\n\ndiscussComponentRegistry.add(\"Message\", Message);\n", "import { Component, onMounted, onWillUnmount, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useMessageActions } from \"./message_actions\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\n\nexport class MessageActionMenuMobile extends Component {\n    static components = { Dialog };\n    static props = [\n        \"message\",\n        \"close?\",\n        \"thread?\",\n        \"isFirstMessage?\",\n        \"messageToReplyTo?\",\n        \"openReactionMenu?\",\n        \"state\",\n    ];\n    static template = \"mail.MessageActionMenuMobile\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.modalRef = useChildRef();\n        this.messageActions = useMessageActions();\n        this.onClickModal = this.onClickModal.bind(this);\n        onMounted(() => {\n            this.modalRef.el.addEventListener(\"click\", this.onClickModal);\n        });\n        onWillUnmount(() => {\n            this.modalRef.el.removeEventListener(\"click\", this.onClickModal);\n        });\n    }\n\n    onClickModal() {\n        this.props.close?.();\n    }\n\n    get message() {\n        return this.props.message;\n    }\n\n    get state() {\n        return this.props.state;\n    }\n\n    async onClickAction(action) {\n        const success = await action.onClick();\n        if (action.mobileCloseAfterClick && (success || success === undefined)) {\n            this.props.close?.();\n        }\n    }\n\n    openReactionMenu() {\n        return this.props.openReactionMenu?.();\n    }\n}\n", "import { Component, toRaw, useComponent, useState, xml } 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 { MessageReactionButton } from \"./message_reaction_button\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { discussComponentRegistry } from \"./discuss_component_registry\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { EMOJI_PICKER_PROPS, EmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { onExternalClick } from \"@mail/utils/common/hooks\";\nimport { convertBrToLineBreak } from \"@mail/utils/common/format\";\n\nconst { DateTime } = luxon;\n\nexport const messageActionsRegistry = registry.category(\"mail.message/actions\");\n\nclass EmojiPickerMobile extends Component {\n    static components = { Dialog, EmojiPicker };\n    static props = [...EMOJI_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'\">\n            <div t-ref=\"root\">\n                <EmojiPicker t-props=\"emojiPickerProps\"/>\n            </div>\n        </Dialog>\n    `;\n\n    get emojiPickerProps() {\n        return {\n            ...this.props,\n            onSelect: (...args) => {\n                this.props.onSelect(...args);\n                this.props.close?.();\n            },\n        };\n    }\n\n    setup() {\n        super.setup();\n        onExternalClick(\"root\", () => this.props.close?.());\n    }\n}\n\nmessageActionsRegistry\n    .add(\"reaction\", {\n        callComponent: MessageReactionButton,\n        props: (component) => ({\n            message: component.props.message,\n            action: messageActionsRegistry.get(\"reaction\"),\n        }),\n        condition: (component) => component.props.message.canAddReaction(component.props.thread),\n        icon: \"oi oi-smile-add\",\n        title: _t(\"Add a Reaction\"),\n        onClick: async (component) => {\n            const def = new Deferred();\n            component.dialog.add(\n                EmojiPickerMobile,\n                {\n                    onSelect: (emoji) => {\n                        const reaction = component.props.message.reactions.find(\n                            ({ content, personas }) =>\n                                content === emoji &&\n                                personas.find((persona) => persona.eq(component.store.self))\n                        );\n                        if (!reaction) {\n                            component.props.message.react(emoji);\n                        }\n                        def.resolve(true);\n                    },\n                },\n                { context: component, onClose: () => def.resolve(false) }\n            );\n            return def;\n        },\n        sequence: 10,\n    })\n    .add(\"reply-to\", {\n        condition: (component) => component.props.message.canReplyTo(component.props.thread),\n        icon: \"fa fa-reply\",\n        title: _t(\"Reply\"),\n        onClick: (component) => {\n            const message = toRaw(component.props.message);\n            const thread = toRaw(component.props.thread);\n            component.props.messageToReplyTo.toggle(thread, message);\n        },\n        sequence: (component) => (component.props.thread?.eq(component.store.inbox) ? 55 : 20),\n    })\n    .add(\"toggle-star\", {\n        condition: (component) => component.props.message.canToggleStar,\n        icon: (component) =>\n            component.props.message.starred ? \"fa fa-star o-mail-Message-starred\" : \"fa fa-star-o\",\n        title: _t(\"Mark as Todo\"),\n        onClick: (component) => component.props.message.toggleStar(),\n        sequence: 30,\n        mobileCloseAfterClick: false,\n    })\n    .add(\"mark-as-read\", {\n        condition: (component) => component.props.thread?.eq(component.store.inbox),\n        icon: \"fa fa-check\",\n        title: _t(\"Mark as Read\"),\n        onClick: (component) => component.props.message.setDone(),\n        sequence: 40,\n    })\n    .add(\"reactions\", {\n        condition: (component) => component.message.reactions.length,\n        icon: \"fa fa-smile-o\",\n        title: _t(\"View Reactions\"),\n        onClick: (component) => component.openReactionMenu(),\n        sequence: 50,\n        dropdown: true,\n    })\n    .add(\"unfollow\", {\n        condition: (component) => component.props.message.canUnfollow(component.props.thread),\n        icon: \"fa-user-times\",\n        title: _t(\"Unfollow\"),\n        onClick: (component) => component.props.message.unfollow(),\n        sequence: 60,\n    })\n    .add(\"mark-as-unread\", {\n        condition: (component) =>\n            component.props.thread?.model === \"discuss.channel\" &&\n            component.store.self.type === \"partner\",\n        icon: \"fa fa-eye-slash\",\n        title: _t(\"Mark as Unread\"),\n        onClick: (component) => component.props.message.onClickMarkAsUnread(component.props.thread),\n        sequence: 70,\n    })\n    .add(\"edit\", {\n        condition: (component) => component.props.message.editable,\n        icon: \"fa fa-pencil\",\n        title: _t(\"Edit\"),\n        onClick: (component) => {\n            const message = toRaw(component.props.message);\n            const text = convertBrToLineBreak(message.body);\n            message.composer = {\n                mentionedPartners: message.recipients,\n                text,\n                selection: {\n                    start: text.length,\n                    end: text.length,\n                    direction: \"none\",\n                },\n            };\n            component.state.isEditing = true;\n        },\n        sequence: 80,\n    })\n    .add(\"delete\", {\n        condition: (component) => component.props.message.editable,\n        icon: \"fa fa-trash\",\n        title: _t(\"Delete\"),\n        onClick: async (component) => {\n            const message = toRaw(component.message);\n            const def = new Deferred();\n            component.dialog.add(\n                discussComponentRegistry.get(\"MessageConfirmDialog\"),\n                {\n                    message,\n                    prompt: _t(\"Are you sure you want to delete this message?\"),\n                    onConfirm: () => {\n                        def.resolve(true);\n                        message.remove();\n                    },\n                },\n                { context: component, onClose: () => def.resolve(false) }\n            );\n            return def;\n        },\n        setup: () => {\n            const component = useComponent();\n            component.dialog = useService(\"dialog\");\n        },\n        sequence: 90,\n    })\n    .add(\"download_files\", {\n        condition: (component) =>\n            component.message.attachment_ids.length > 1 && component.store.self.isInternalUser,\n        icon: \"fa fa-download\",\n        title: _t(\"Download Files\"),\n        onClick: (component) =>\n            download({\n                data: {\n                    file_ids: component.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    })\n    .add(\"toggle-translation\", {\n        condition: (component) => component.props.message.isTranslatable(component.props.thread),\n        icon: (component) =>\n            `fa fa-language ${component.state.showTranslation ? \"o-mail-Message-translated\" : \"\"}`,\n        title: (component) => (component.state.showTranslation ? _t(\"Revert\") : _t(\"Translate\")),\n        onClick: (component) => component.onClickToggleTranslation(),\n        sequence: 100,\n    })\n    .add(\"copy-link\", {\n        condition: (component) =>\n            component.message.message_type &&\n            component.message.message_type !== \"user_notification\",\n        icon: \"fa fa-link\",\n        title: _t(\"Copy Link\"),\n        onClick: (component) => component.message.copyLink(),\n        sequence: 110,\n    });\n\nfunction transformAction(component, id, action) {\n    return {\n        component: action.component,\n        id,\n        mobileCloseAfterClick: action.mobileCloseAfterClick ?? true,\n        /** Condition to display this action. */\n        get condition() {\n            return action.condition(component);\n        },\n        /** Icon for the button this action. */\n        get icon() {\n            return typeof action.icon === \"function\" ? action.icon(component) : action.icon;\n        },\n        /** title of this action, displayed to the user. */\n        get title() {\n            return typeof action.title === \"function\" ? action.title(component) : action.title;\n        },\n        callComponent: action.callComponent,\n        get props() {\n            return action.props(component);\n        },\n        /**\n         * Action to execute when this action is click.\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        onClick() {\n            return action.onClick?.(component);\n        },\n        /** Determines the order of this action (smaller first). */\n        get sequence() {\n            return typeof action.sequence === \"function\"\n                ? action.sequence(component)\n                : action.sequence;\n        },\n        /** Component setup to execute when this action is registered. */\n        setup: action.setup,\n    };\n}\n\nexport function useMessageActions() {\n    const component = useComponent();\n    const transformedActions = messageActionsRegistry\n        .getEntries()\n        .map(([id, action]) => transformAction(component, id, action));\n    for (const action of transformedActions) {\n        if (action.setup) {\n            action.setup(action);\n        }\n    }\n    const state = useState({\n        get actions() {\n            const actions = transformedActions\n                .filter((action) => action.condition)\n                .sort((a1, a2) => a1.sequence - a2.sequence);\n            if (actions.length > 0) {\n                actions.at(0).isFirst = true;\n                actions.at(-1).isLast = true;\n            }\n            return actions;\n        },\n    });\n    return state;\n}\n", "import { Message } from \"@mail/core/common/message\";\nimport { useVisible } from \"@mail/utils/common/hooks\";\n\nimport { Component, useState, 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 = useState(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) {\n            this.env.pinMenu?.close();\n            this.env.searchMenu?.close();\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(\"Confirm\"),\n        size: \"xl\",\n        title: _t(\"Confirmation\"),\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, useState } 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 = [\"message\", \"onClick?\"];\n    static template = \"mail.MessageInReply\";\n\n    setup() {\n        super.setup();\n        this.store = useState(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            ![\"partner\", \"guest\"].includes(this.props.message.author?.type)\n        ) {\n            return url(\"/mail/static/src/img/email_icon.png\");\n        }\n\n        if (this.props.message.parentMessage.author) {\n            return this.props.message.parentMessage.author.avatarUrl;\n        }\n\n        return this.store.DEFAULT_AVATAR;\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\nimport {\n    EMOJI_REGEX,\n    convertBrToLineBreak,\n    htmlToTextContentInline,\n    prettifyMessageContent,\n} from \"@mail/utils/common/format\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { url } from \"@web/core/utils/urls\";\nimport { stateToUrl } from \"@web/core/browser/router\";\nimport { toRaw } from \"@odoo/owl\";\n\nconst { DateTime } = luxon;\nexport class Message extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").Message>} */\n    static records = {};\n    /** @returns {import(\"models\").Message} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Message|import(\"models\").Message[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @param {Object} data */\n    update(data) {\n        super.update(data);\n        if (this.isNotification && !this.notificationType) {\n            const parser = new DOMParser();\n            const htmlBody = parser.parseFromString(this.body, \"text/html\");\n            this.notificationType = htmlBody.querySelector(\".o_mail_notification\")?.dataset.oeType;\n        }\n    }\n\n    attachment_ids = Record.many(\"Attachment\", { inverse: \"message\" });\n    author = Record.one(\"Persona\");\n    body = Record.attr(\"\", { html: true });\n    composer = Record.one(\"Composer\", { inverse: \"message\", onDelete: (r) => r.delete() });\n    /** @type {DateTime} */\n    date = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {string} */\n    default_subject;\n    /** @type {boolean} */\n    edited = Record.attr(false, {\n        compute() {\n            return Boolean(\n                new DOMParser()\n                    .parseFromString(this.body, \"text/html\")\n                    // \".o-mail-Message-edited\" is the class added by the mail.thread in _message_update_content\n                    // when the message is edited\n                    .querySelector(\".o-mail-Message-edited\")\n            );\n        },\n    });\n    hasEveryoneSeen = Record.attr(false, {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            return this.thread?.membersThatCanSeen.every((m) => m.hasSeen(this));\n        },\n    });\n    isMessagePreviousToLastSelfMessageSeenByEveryone = Record.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    isReadBySelf = Record.attr(false, {\n        compute() {\n            return (\n                this.thread?.selfMember?.seen_message_id?.id >= this.id &&\n                this.thread?.selfMember?.new_message_separator > this.id\n            );\n        },\n    });\n    hasSomeoneSeen = Record.attr(false, {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            return this.thread?.membersThatCanSeen\n                .filter(({ persona }) => !persona.eq(this.author))\n                .some((m) => m.hasSeen(this));\n        },\n    });\n    hasSomeoneFetched = Record.attr(false, {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            if (!this.thread) {\n                return false;\n            }\n            const otherFetched = this.thread.channelMembers.filter(\n                (m) => m.persona.notEq(this.author) && m.fetched_message_id?.id >= this.id\n            );\n            return otherFetched.length > 0;\n        },\n    });\n    hasLink = Record.attr(false, {\n        compute() {\n            if (this.isBodyEmpty) {\n                return false;\n            }\n            const div = document.createElement(\"div\");\n            div.innerHTML = this.body;\n            return Boolean(div.querySelector(\"a:not([data-oe-model])\"));\n        },\n    });\n    /** @type {number|string} */\n    id;\n    /** @type {boolean} */\n    is_discussion;\n    /** @type {boolean} */\n    is_note;\n    /** @type {boolean} */\n    is_transient;\n    linkPreviews = Record.many(\"LinkPreview\", { inverse: \"message\", onDelete: (r) => r.delete() });\n    /** @type {number[]} */\n    parentMessage = Record.one(\"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 = Record.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    notifications = Record.many(\"Notification\", { inverse: \"message\" });\n    recipients = Record.many(\"Persona\");\n    thread = Record.one(\"Thread\");\n    threadAsNeedaction = Record.one(\"Thread\", {\n        compute() {\n            if (this.needaction) {\n                return this.thread;\n            }\n        },\n    });\n    threadAsNewest = Record.one(\"Thread\");\n    /** @type {DateTime} */\n    scheduledDatetime = Record.attr(undefined, { type: \"datetime\" });\n    onlyEmojis = Record.attr(false, {\n        compute() {\n            const div = document.createElement(\"div\");\n            div.innerHTML = this.body;\n            const bodyWithoutTags = div.textContent;\n            const withoutEmojis = bodyWithoutTags.replace(EMOJI_REGEX, \"\");\n            return bodyWithoutTags.length > 0 && withoutEmojis.trim().length === 0;\n        },\n    });\n    /** @type {string} */\n    subject;\n    /** @type {string} */\n    subtype_description;\n    threadAsFirstUnread = Record.one(\"Thread\", { inverse: \"firstUnreadMessage\" });\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    /** @type {luxon.DateTime} */\n    create_date = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {luxon.DateTime} */\n    write_date = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {undefined|Boolean} */\n    needaction;\n    starred = false;\n\n    /**\n     * True if the backend would technically allow edition\n     * @returns {boolean}\n     */\n    get allowsEdition() {\n        return this.store.self.isAdmin || this.isSelfAuthored;\n    }\n\n    get bubbleColor() {\n        if (!this.isSelfAuthored && !this.is_note && !this.isHighlightedFromMention) {\n            return \"blue\";\n        }\n        if (this.isSelfAuthored && !this.is_note && !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.allowsEdition) {\n            return false;\n        }\n        return this.message_type === \"comment\";\n    }\n\n    get dateDay() {\n        let dateDay = this.datetime.toLocaleString(DateTime.DATE_FULL);\n        if (dateDay === DateTime.now().toLocaleString(DateTime.DATE_FULL)) {\n            dateDay = _t(\"Today\");\n        }\n        return dateDay;\n    }\n\n    get dateSimple() {\n        return this.datetime.toLocaleString(DateTime.TIME_24_SIMPLE, {\n            locale: user.lang,\n        });\n    }\n\n    get datetime() {\n        return this.date || DateTime.now();\n    }\n\n    get datetimeShort() {\n        return this.datetime.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS);\n    }\n\n    get isSelfMentioned() {\n        return this.store.self.in(this.recipients);\n    }\n\n    get isHighlightedFromMention() {\n        return this.isSelfMentioned && this.thread?.model === \"discuss.channel\";\n    }\n\n    isSelfAuthored = Record.attr(false, {\n        compute() {\n            if (!this.author) {\n                return false;\n            }\n            return this.author.eq(this.store.self);\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.name) {\n            return false;\n        }\n        const regexPrefix = /^((re|fw|fwd)\\s*:\\s*)*/i;\n        const cleanedThreadName = this.thread.name.replace(regexPrefix, \"\");\n        const cleanedSubject = this.subject.replace(regexPrefix, \"\");\n        return cleanedSubject === cleanedThreadName;\n    }\n\n    get isSubjectDefault() {\n        const name = this.thread?.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 resUrl() {\n        return url(stateToUrl({ model: this.thread.model, resId: this.thread.id }));\n    }\n\n    isTranslatable(thread) {\n        return (\n            this.store.hasMessageTranslationFeature &&\n            ![\"discuss.channel\", \"mail.box\"].includes(thread?.model)\n        );\n    }\n\n    get hasTextContent() {\n        return !this.isBodyEmpty;\n    }\n\n    isEmpty = Record.attr(false, {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            return (\n                this.isBodyEmpty &&\n                this.attachment_ids.length === 0 &&\n                this.trackingValues.length === 0 &&\n                !this.subtype_description\n            );\n        },\n    });\n    isBodyEmpty = Record.attr(undefined, {\n        compute() {\n            return (\n                !this.body ||\n                [\"\", \"<p></p>\", \"<p><br></p>\", \"<p><br/></p>\"].includes(\n                    this.body\n                        .replace('<span class=\"o-mail-Message-edited\"></span>', \"\")\n                        .replace(/\\s/g, \"\")\n                )\n            );\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.linkPreviews.length === 1 &&\n            this.linkPreviews[0].isImage\n        );\n    }\n\n    get inlineBody() {\n        if (!this.body) {\n            return \"\";\n        }\n        return htmlToTextContentInline(this.body);\n    }\n\n    get notificationIcon() {\n        switch (this.notificationType) {\n            case \"pin\":\n                return \"fa fa-thumb-tack\";\n        }\n        return null;\n    }\n\n    get failureNotifications() {\n        return this.notifications.filter((notification) => notification.isFailure);\n    }\n\n    get scheduledDateSimple() {\n        return this.scheduledDatetime.toLocaleString(DateTime.TIME_24_SIMPLE, {\n            locale: user.lang,\n        });\n    }\n\n    get canToggleStar() {\n        return Boolean(\n            !this.is_transient &&\n                this.thread &&\n                this.store.self.type === \"partner\" &&\n                this.store.self.isInternalUser\n        );\n    }\n\n    /** @param {import(\"models\").Thread} thread the thread where the message is shown */\n    canAddReaction(thread) {\n        return Boolean(!this.is_transient && this.thread);\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 edit(body, attachments = [], { mentionedChannels = [], mentionedPartners = [] } = {}) {\n        if (convertBrToLineBreak(this.body) === body && attachments.length === 0) {\n            return;\n        }\n        const validMentions = this.store.getMentionsFromText(body, {\n            mentionedChannels,\n            mentionedPartners,\n        });\n        const data = await rpc(\"/mail/message/update_content\", {\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.access_token),\n            body: await prettifyMessageContent(body, validMentions),\n            message_id: this.id,\n            partner_ids: validMentions?.partners?.map((partner) => partner.id),\n            ...this.thread.rpcParams,\n        });\n        this.store.insert(data, { html: true });\n        if (this.hasLink && this.store.hasLinkPreviewFeature) {\n            rpc(\"/mail/link_preview\", { message_id: this.id }, { silent: true });\n        }\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() {\n        await rpc(\"/mail/message/update_content\", {\n            attachment_ids: [],\n            attachment_tokens: [],\n            body: \"\",\n            message_id: this.id,\n            ...this.thread.rpcParams,\n        });\n        this.body = \"\";\n        this.attachment_ids = [];\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        await this.store.env.services.orm.silent.call(\"mail.message\", \"toggle_message_starred\", [\n            [this.id],\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\".', { thread_name: thread.name }),\n            { type: \"success\" }\n        );\n    }\n\n    get channelMemberHaveSeen() {\n        return this.thread.membersThatCanSeen.filter(\n            (m) => m.hasSeen(this) && m.persona.notEq(this.author)\n        );\n    }\n\n    /** @param {import(\"models\").Thread} thread the thread where the message is shown */\n    onClickMarkAsUnread(thr) {\n        const message = toRaw(this);\n        const thread = toRaw(thr);\n        if (!thread.selfMember || thread.selfMember?.new_message_separator === message.id) {\n            return;\n        }\n        return rpc(\"/discuss/channel/mark_as_unread\", {\n            channel_id: message.thread.id,\n            message_id: message.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 { Component, useRef, useState } from \"@odoo/owl\";\nimport { useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Message} message\n * @extends {Component<Props, Env>}\n */\nexport class MessageReactionButton extends Component {\n    static template = \"mail.MessageReactionButton\";\n    static props = [\"message\", \"classNames?\", \"action\"];\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.emojiPickerRef = useRef(\"emoji-picker\");\n        this.emojiPicker = useEmojiPicker(this.emojiPickerRef, {\n            onSelect: (emoji) => {\n                const reaction = this.props.message.reactions.find(\n                    ({ content, personas }) =>\n                        content === emoji && personas.find((persona) => persona.eq(this.store.self))\n                );\n                if (!reaction) {\n                    this.props.message.react(emoji);\n                }\n            },\n        });\n    }\n}\n", "import { useHover } from \"@mail/utils/common/hooks\";\nimport { Component, onMounted, onPatched, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { loadEmoji, loader } 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 = useState(useService(\"mail.store\"));\n        this.ui = useService(\"ui\");\n        this.hover = useHover([\"reactionButton\", \"reactionList*\"], {\n            onHover: () => (this.preview.isOpen = true),\n            onAway: () => (this.preview.isOpen = false),\n        });\n        this.state = useState({ emojiLoaded: Boolean(loader.loaded) });\n        if (!loader.loaded) {\n            loader.onEmojiLoaded(() => (this.state.emojiLoaded = true));\n        }\n        onMounted(() => void this.state.emojiLoaded);\n        onPatched(() => void this.state.emojiLoaded);\n        this.preview = useDropdownState();\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 => persona.name);\n        const shortcode = loader.loaded?.emojiValueToShortcode?.[emoji] ?? \"?\";\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.store.self.in(reaction.personas);\n    }\n\n    onClickReaction(reaction) {\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", "import { loadEmoji, loader } from \"@web/core/emoji_picker/emoji_picker\";\nimport { onExternalClick } from \"@mail/utils/common/hooks\";\n\nimport {\n    Component,\n    onMounted,\n    onPatched,\n    useEffect,\n    useExternalListener,\n    useRef,\n    useState,\n} 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 = useState(useService(\"mail.store\"));\n        this.ui = useState(useService(\"ui\"));\n        this.state = useState({\n            emojiLoaded: Boolean(loader.loaded),\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(async () => {\n            if (!loader.loaded) {\n                loadEmoji();\n            }\n        });\n        if (!loader.loaded) {\n            loader.onEmojiLoaded(() => (this.state.emojiLoaded = true));\n        }\n        onMounted(() => void this.state.emojiLoaded);\n        onPatched(() => void this.state.emojiLoaded);\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 loader.loaded?.emojiValueToShortcode?.[reaction.content] ?? \"?\";\n    }\n}\n", "import { Component, useRef, useState } from \"@odoo/owl\";\n\nimport { MessageReactionList } from \"@mail/core/common/message_reaction_list\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\n\nexport class MessageReactions extends Component {\n    static props = [\"message\", \"openReactionMenu\"];\n    static template = \"mail.MessageReactions\";\n    static components = { MessageReactionList };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.ui = useService(\"ui\");\n        this.addRef = useRef(\"add\");\n        this.emojiPicker = useEmojiPicker(this.addRef, {\n            onSelect: (emoji) => {\n                const reaction = this.props.message.reactions.find(\n                    ({ content, personas }) =>\n                        content === emoji && personas.find((persona) => persona.eq(this.store.self))\n                );\n                if (!reaction) {\n                    this.props.message.react(emoji);\n                }\n            },\n        });\n    }\n}\n", "import { AND, 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    /** @returns {import(\"models\").MessageReactions} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").MessageReactions|import(\"models\").MessageReactions[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @type {string} */\n    content;\n    /** @type {number} */\n    count;\n    /** @type {number} */\n    sequence;\n    personas = Record.many(\"Persona\");\n    message = Record.one(\"Message\");\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 { 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 = new DOMParser().parseFromString(target, \"text/html\");\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.search(this.searchTerm, this.thread, before)\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        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 { 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.channelMembers.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 { 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        autoSelectFirst: { type: Boolean, optional: true },\n        class: { type: String, optional: true },\n        hint: { 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        isLoading: { type: Boolean, optional: true },\n    };\n    static defaultProps = { position: \"bottom\", isLoading: false, autoSelectFirst: true };\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 (\n                isEventHandled(ev, \"composer.onClickTextarea\") ||\n                isEventHandled(ev, \"channelSelector.onClickInput\")\n            ) {\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        if (this.props.autoSelectFirst) {\n            this.navigate(\"first\");\n        }\n    }\n\n    close() {\n        this.state.open = false;\n        this.state.activeIndex = null;\n    }\n\n    selectOption(ev, index, params = {}) {\n        const option = this.props.options[index];\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        this.state.activeIndex = index;\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class Notification extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").Notification>} */\n    static records = {};\n    /** @returns {import(\"models\").Notification} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Notification|import(\"models\").Notification[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @type {number} */\n    id;\n    message = Record.one(\"Message\");\n    /** @type {string} */\n    notification_status;\n    /** @type {string} */\n    notification_type;\n    failure = Record.one(\"Failure\", {\n        inverse: \"notifications\",\n        /** @this {import(\"models\").Notification} */\n        compute() {\n            const thread = this.message?.thread;\n            if (!this.message?.isSelfAuthored) {\n                return;\n            }\n            const failure = Object.values(this.store.Failure.records).find((f) => {\n                return (\n                    f.resModel === thread?.model &&\n                    f.type === this.notification_type &&\n                    (f.resModel !== \"discuss.channel\" || f.resIds.has(thread?.id))\n                );\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    persona = Record.one(\"Persona\");\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 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-exclamation\";\n            case \"ready\":\n                return \"fa fa-send-o\";\n            case \"canceled\":\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(\"Ready\");\n            case \"canceled\":\n                return _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 { isAndroidApp, isIosApp } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\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 {Partial<import(\"services\").Services>} services\n     */\n    async start(env, services) {\n        const notification = services.notification;\n        let permission;\n        try {\n            permission = await browser.navigator?.permissions?.query({\n                name: \"notifications\",\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) {\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 { htmlToTextContentInline } from \"@mail/utils/common/format\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { url } from \"@web/core/utils/urls\";\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 {Partial<import(\"services\").Services>} 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.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        if (!author) {\n            notificationTitle = _t(\"New message\");\n        } else {\n            if (message.thread?.channel_type === \"channel\") {\n                notificationTitle = _t(\"%(author name)s from %(channel name)s\", {\n                    \"author name\": author.name,\n                    \"channel name\": message.thread.displayName,\n                });\n            } else {\n                notificationTitle = author.name;\n            }\n        }\n        const notificationContent = htmlToTextContentInline(message.body).substring(\n            0,\n            PREVIEW_MSG_MAX_SIZE\n        );\n        this.sendNotification({\n            message: notificationContent,\n            sound: message.thread?.model === \"discuss.channel\",\n            title: notificationTitle,\n            type: \"info\",\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     */\n    sendNotification({ message, sound = true, title, type }) {\n        if (!this.canSendNativeNotification) {\n            this.sendOdooNotification(message, { sound, title, type });\n            return;\n        }\n        if (!this.multiTab.isOnMainTab()) {\n            return;\n        }\n        try {\n            this.sendNativeNotification(title, message, { 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, { sound = true } = {}) {\n        const notification = new Notification(title, {\n            body: message,\n            icon: \"/mail/static/src/img/odoobot_transparent.png\",\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 (this.canPlayAudio && this.multiTab.isOnMainTab()) {\n            if (!this.audio) {\n                this.audio = new Audio();\n                this.audio.src = this.audio.canPlayType(\"audio/ogg; codecs=vorbis\")\n                    ? url(\"/mail/static/src/audio/ting.ogg\")\n                    : url(\"/mail/static/src/audio/ting.mp3\");\n            }\n            try {\n                await this.audio.play();\n            } catch {\n                // Ignore errors due to the user not having interracted\n                // with the page before playing the sound.\n            }\n        }\n    }\n\n    get canPlayAudio() {\n        return typeof Audio !== \"undefined\";\n    }\n\n    get canSendNativeNotification() {\n        return Boolean(browser.Notification && browser.Notification.permission === \"granted\");\n    }\n}\n\nexport const outOfFocusService = {\n    dependencies: [\"multi_tab\", \"notification\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} 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.isInternalUser;\n        const isBInternalUser = p2.isInternalUser;\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));\n            const isFollower2 = followerList.some((follower) => p2.eq(follower.partner));\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(\n    \"mail.id\",\n    (p1, p2) => {\n        return p1.id - p2.id;\n    },\n    { sequence: 75 }\n);\n", "import { AND, Record } from \"@mail/core/common/record\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { rpc } from \"@web/core/network/rpc\";\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 {'partner'|'guest'} type\n * @property {ImStatus} im_status\n */\n\nexport class Persona extends Record {\n    static id = AND(\"type\", \"id\");\n    /** @type {Object.<number, import(\"models\").Persona>} */\n    static records = {};\n    /** @returns {import(\"models\").Persona} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Persona|import(\"models\").Persona[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    channelMembers = Record.many(\"ChannelMember\");\n    /** @type {number} */\n    id;\n    /** @type {boolean | undefined} */\n    is_company;\n    /** @type {string} */\n    landlineNumber;\n    /** @type {string} */\n    mobileNumber;\n    storeAsTrackedImStatus = Record.one(\"Store\", {\n        /** @this {import(\"models\").Persona} */\n        compute() {\n            if (\n                this.type === \"guest\" ||\n                (this.type === \"partner\" && this.im_status !== \"im_partner\" && !this.is_public)\n            ) {\n                return this.store;\n            }\n        },\n        onAdd() {\n            if (!this.store.env.services.bus_service.isActive) {\n                return;\n            }\n            const model = this.type === \"partner\" ? \"res.partner\" : \"mail.guest\";\n            this.store.env.services.bus_service.addChannel(`odoo-presence-${model}_${this.id}`);\n        },\n        onDelete() {\n            if (!this.store.env.services.bus_service.isActive) {\n                return;\n            }\n            const model = this.type === \"partner\" ? \"res.partner\" : \"mail.guest\";\n            this.store.env.services.bus_service.deleteChannel(`odoo-presence-${model}_${this.id}`);\n        },\n        eager: true,\n        inverse: \"imStatusTrackedPersonas\",\n    });\n    /** @type {'partner' | 'guest'} */\n    type;\n    /** @type {string} */\n    name;\n    country = Record.one(\"Country\");\n    /** @type {string} */\n    email;\n    /** @type {number} */\n    userId;\n    /** @type {ImStatus} */\n    im_status;\n    /** @type {'email' | 'inbox'} */\n    notification_preference;\n    isAdmin = false;\n    isInternalUser = false;\n    /** @type {luxon.DateTime} */\n    write_date = Record.attr(undefined, { type: \"datetime\" });\n\n    /**\n     * @returns {boolean}\n     */\n    get hasPhoneNumber() {\n        return Boolean(this.mobileNumber || this.landlineNumber);\n    }\n\n    get emailWithoutDomain() {\n        return this.email.substring(0, this.email.lastIndexOf(\"@\"));\n    }\n\n    get avatarUrl() {\n        if (this.type === \"partner\") {\n            return imageUrl(\"res.partner\", this.id, \"avatar_128\", { unique: this.write_date });\n        }\n        if (this.type === \"guest\") {\n            return imageUrl(\"mail.guest\", this.id, \"avatar_128\", { unique: this.write_date });\n        }\n        if (this.userId) {\n            return imageUrl(\"res.users\", this.userId, \"avatar_128\", { unique: this.write_date });\n        }\n        return this.store.DEFAULT_AVATAR;\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    async updateGuestName(name) {\n        await rpc(\"/mail/guest/update_name\", {\n            guest_id: this.id,\n            name,\n        });\n    }\n}\n\nPersona.register();\n", "import { Component, useExternalListener, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { isEventHandled } from \"@web/core/utils/misc\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { PickerContent } from \"@mail/core/common/picker_content\";\nimport { useLazyExternalListener } from \"@mail/utils/common/hooks\";\n\nexport function usePicker(setting) {\n    const storeScroll = {\n        scrollValue: 0,\n        set: (value) => (storeScroll.scrollValue = value),\n        get: () => storeScroll.scrollValue,\n    };\n    const PICKERS = {\n        NONE: \"none\",\n        EMOJI: \"emoji\",\n        GIF: \"gif\",\n    };\n    return useState({\n        PICKERS,\n        anchor: setting.anchor,\n        buttons: setting.buttons,\n        close: setting.close,\n        pickers: setting.pickers,\n        position: setting.position,\n        state: {\n            picker: PICKERS.NONE,\n            searchTerm: \"\",\n        },\n        storeScroll,\n    });\n}\n\n/**\n * Picker/usePicker is a component hook that can be used to display the emoji picker/gif picker (if it is enabled).\n * It can be used in two ways:\n * - with a popover when in large screen: the picker will be displayed in a popover triggered by provided buttons.\n * - with a keyboard when in mobile view: the picker will be displayed in place where the Picker component is placed.\n * The switch between the two modes is done automatically based on the screen size.\n */\n\nexport class Picker extends Component {\n    static components = {\n        PickerContent,\n    };\n    static props = [\n        \"PICKERS\",\n        \"anchor?\",\n        \"buttons\",\n        \"close?\",\n        \"state\",\n        \"pickers\",\n        \"position?\",\n        \"storeScroll\",\n        \"fixed?\",\n    ];\n    static template = \"mail.Picker\";\n\n    setup() {\n        this.ui = useState(useService(\"ui\"));\n        this.popover = usePopover(PickerContent, this.popoverSettings);\n        useExternalListener(\n            browser,\n            \"click\",\n            async (ev) => {\n                if (this.props.state.picker === this.props.PICKERS.NONE) {\n                    return;\n                }\n                await new Promise(setTimeout); // let bubbling to catch marked event handled\n                if (!this.isEventHandledByPicker(ev)) {\n                    this.close();\n                }\n            },\n            true\n        );\n        for (const button of this.props.buttons) {\n            useLazyExternalListener(\n                () => button.el,\n                \"click\",\n                async (ev) => this.toggle(this.props.anchor?.el ?? button.el, ev)\n            );\n        }\n    }\n\n    get popoverSettings() {\n        return {\n            position: this.props.position,\n            fixedPosition: this.props.fixed,\n            onClose: () => this.close(),\n            closeOnClickAway: false,\n            animation: false,\n            arrow: false,\n        };\n    }\n\n    get contentProps() {\n        const pickers = {};\n        for (const [name, fn] of Object.entries(this.props.pickers)) {\n            pickers[name] = (str, resetOnSelect) => {\n                fn(str);\n                if (resetOnSelect) {\n                    this.close();\n                }\n            };\n        }\n        return {\n            PICKERS: this.props.PICKERS,\n            close: () => this.close(),\n            pickers,\n            state: this.props.state,\n            storeScroll: this.props.storeScroll,\n        };\n    }\n\n    /**\n     * @param {Event} ev\n     * @returns {boolean}\n     */\n    isEventHandledByPicker(ev) {\n        return (\n            isEventHandled(ev, \"Composer.onClickAddEmoji\") ||\n            isEventHandled(ev, \"PickerContent.onClick\")\n        );\n    }\n\n    async toggle(el, ev) {\n        // Let event be handled by bubbling handlers first.\n        await new Promise(setTimeout);\n        // In small screen, we toggle keyboard picker.\n        if (this.ui.isSmall) {\n            if (this.props.state.picker === this.props.PICKERS.NONE) {\n                this.props.state.picker = this.props.PICKERS.EMOJI;\n            } else {\n                this.props.state.picker = this.props.PICKERS.NONE;\n            }\n            return;\n        }\n        // In large screen, we toggle popover.\n        if (isEventHandled(ev, \"Composer.onClickAddEmoji\")) {\n            if (this.popover.isOpen) {\n                if (this.props.state.picker === this.props.PICKERS.EMOJI) {\n                    this.props.state.picker = this.props.PICKERS.NONE;\n                    this.popover.close();\n                    return;\n                }\n                this.props.state.picker = this.props.PICKERS.EMOJI;\n            } else {\n                this.props.state.picker = this.props.PICKERS.EMOJI;\n                this.popover.open(el, this.contentProps);\n            }\n        }\n    }\n\n    close() {\n        this.props.close?.();\n        this.popover.close();\n        this.props.state.picker = this.props.PICKERS.NONE;\n        this.props.state.searchTerm = \"\";\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\nimport { EmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\n\n/**\n * PickerContent is the content displayed in the popover/Picker.\n * It is used to display the emoji picker/gif picker (if it is enabled).\n */\nexport class PickerContent extends Component {\n    static components = { EmojiPicker };\n    static props = [\"PICKERS\", \"close\", \"pickers\", \"state\", \"storeScroll\"];\n    static template = \"mail.PickerContent\";\n\n    onClick(ev) {\n        markEventHandled(ev, \"PickerContent.onClick\");\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 { Component, onWillUpdateProps, useExternalListener, useState } from \"@odoo/owl\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\nimport { useMessageSearch } from \"@mail/core/common/message_search_hook\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\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 {string} [className]\n * @property {funtion} [closeSearch]\n * @property {funtion} [onClickJump]\n * @extends {Component<Props, Env>}\n */\nexport class SearchMessagesPanel extends Component {\n    static components = {\n        MessageCardList,\n        ActionPanel,\n    };\n    static props = [\"thread\", \"className?\", \"closeSearch?\", \"onClickJump?\"];\n    static template = \"mail.SearchMessagesPanel\";\n\n    setup() {\n        super.setup();\n        this.state = useState({ searchTerm: \"\", searchedTerm: \"\" });\n        this.messageSearch = useMessageSearch(this.props.thread);\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        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 messages\");\n    }\n\n    get MESSAGES_FOUND() {\n        if (this.messageSearch.messages.length === 0) {\n            return false;\n        }\n        return _t(\"%s messages found\", this.messageSearch.count);\n    }\n\n    search() {\n        this.messageSearch.searchTerm = this.state.searchTerm;\n        this.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.messageSearch.clear();\n        this.props.closeSearch?.();\n    }\n\n    /** @param {KeyboardEvent} ev */\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    onLoadMoreVisible() {\n        const before = this.messageSearch.messages\n            ? Math.min(...this.messageSearch.messages.map((message) => message.id))\n            : false;\n        this.messageSearch.search(before);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { Record } from \"./record\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class Settings extends Record {\n    id;\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    // Notification settings\n    /**\n     * @type {\"mentions\"|\"all\"|\"no_notif\"}\n     */\n    channel_notifications = Record.attr(\"mentions\", {\n        compute() {\n            return this.channel_notifications === false ? \"mentions\" : this.channel_notifications;\n        },\n    });\n    mute_until_dt = Record.attr(false, { type: \"datetime\" });\n\n    // Voice settings\n    // DeviceId of the audio input selected by the user\n    audioInputDeviceId = \"\";\n    use_push_to_talk = false;\n    voice_active_duration = 200;\n    volumes = Record.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 = false;\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 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                ? sprintf(_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} 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.type !== \"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.showOnlyVideo =\n            browser.localStorage.getItem(\"mail_user_setting_show_only_video\") === \"true\";\n        this.useBlur = browser.localStorage.getItem(\"mail_user_setting_use_blur\") === \"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    }\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    /**\n     * @private\n     */\n    async _saveSettings() {\n        if (this.store.self.type !== \"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            \"channel-join\": { defaultVolume: 0.3, path: \"/mail/static/src/audio/channel_01_in\" },\n            \"channel-leave\": { path: \"/mail/static/src/audio/channel_04_out\" },\n            deafen: { defaultVolume: 0.15, path: \"/mail/static/src/audio/deafen_new_01\" },\n            \"incoming-call\": { defaultVolume: 0.15, path: \"/mail/static/src/audio/call_02_in_\" },\n            \"member-leave\": { defaultVolume: 0.5, path: \"/mail/static/src/audio/channel_01_out\" },\n            mute: { defaultVolume: 0.2, path: \"/mail/static/src/audio/mute_1\" },\n            \"new-message\": { path: \"/mail/static/src/audio/dm_02\" },\n            \"push-to-talk-on\": { defaultVolume: 0.05, path: \"/mail/static/src/audio/ptt_push_1\" },\n            \"push-to-talk-off\": {\n                defaultVolume: 0.05,\n                path: \"/mail/static/src/audio/ptt_release_1\",\n            },\n            \"screen-sharing\": { defaultVolume: 0.5, path: \"/mail/static/src/audio/share_02\" },\n            undeafen: { defaultVolume: 0.15, path: \"/mail/static/src/audio/undeafen_new_01\" },\n            unmute: { defaultVolume: 0.2, path: \"/mail/static/src/audio/unmute_1\" },\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 { compareDatetime } from \"@mail/utils/common/misc\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Store as BaseStore, makeStore, Record } from \"@mail/core/common/record\";\nimport { reactive } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { Deferred, Mutex } from \"@web/core/utils/concurrency\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { session } from \"@web/session\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { cleanTerm, prettifyMessageContent } from \"@mail/utils/common/format\";\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.member\": \"ChannelMember\",\n    \"discuss.channel.rtc.session\": \"RtcSession\",\n    \"discuss.channel\": \"Thread\",\n    \"ir.attachment\": \"Attachment\",\n    \"mail.activity\": \"Activity\",\n    \"mail.guest\": \"Persona\",\n    \"mail.followers\": \"Follower\",\n    \"mail.link.preview\": \"LinkPreview\",\n    \"mail.message\": \"Message\",\n    \"mail.notification\": \"Notification\",\n    \"mail.scheduled.message\": \"ScheduledMessage\",\n    \"mail.thread\": \"Thread\",\n    \"res.partner\": \"Persona\",\n};\n\nexport const addFieldsByPyModel = {\n    \"discuss.channel\": { model: \"discuss.channel\" },\n    \"mail.guest\": { type: \"guest\" },\n    \"res.partner\": { type: \"partner\" },\n};\n\nexport class Store extends BaseStore {\n    static FETCH_DATA_DEBOUNCE_DELAY = 1;\n    static OTHER_LONG_TYPING = 60000;\n    FETCH_LIMIT = 30;\n    DEFAULT_AVATAR = \"/mail/static/src/img/smiley/avatar.jpg\";\n    isReady = new Deferred();\n\n    /** @returns {import(\"models\").Store|import(\"models\").Store[]} */\n    static insert() {\n        return super.insert(...arguments);\n    }\n\n    /** @type {typeof import(\"@mail/core/web/activity_model\").Activity} */\n    Activity;\n    /** @type {typeof import(\"@mail/core/common/attachment_model\").Attachment} */\n    Attachment;\n    /** @type {typeof import(\"@mail/core/common/canned_response_model\").CannedResponse} */\n    [\"mail.canned.response\"];\n    /** @type {typeof import(\"@mail/core/common/channel_member_model\").ChannelMember} */\n    ChannelMember;\n    /** @type {typeof import(\"@mail/core/common/chat_window_model\").ChatWindow} */\n    ChatWindow;\n    /** @type {typeof import(\"@mail/core/common/composer_model\").Composer} */\n    Composer;\n    /** @type {typeof import(\"@mail/core/common/failure_model\").Failure} */\n    Failure;\n    /** @type {typeof import(\"@mail/core/common/follower_model\").Follower} */\n    Follower;\n    /** @type {typeof import(\"@mail/core/common/link_preview_model\").LinkPreview} */\n    LinkPreview;\n    /** @type {typeof import(\"@mail/core/common/message_model\").Message} */\n    Message;\n    /** @type {typeof import(\"@mail/core/common/message_reactions_model\").MessageReactions} */\n    MessageReactions;\n    /** @type {typeof import(\"@mail/core/common/notification_model\").Notification} */\n    Notification;\n    /** @type {typeof import(\"@mail/core/common/persona_model\").Persona} */\n    Persona;\n    /** @type {typeof import \"@mail/chatter/web/scheduled_message_model).ScheduledMessage\"} */\n    ScheduledMessage;\n    /** @type {typeof import(\"@mail/core/common/settings_model\").Settings} */\n    Settings;\n    /** @type {typeof import(\"@mail/core/common/thread_model\").Thread} */\n    Thread;\n    /** @type {typeof import(\"@mail/core/common/volume_model\").Volume} */\n    Volume;\n\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    channel_types_with_seen_infos = [];\n    /** This is the current logged partner / guest */\n    self = Record.one(\"Persona\");\n    /**\n     * Indicates whether the current user is using the application through the\n     * public page.\n     */\n    inPublicPage = false;\n    odoobot = Record.one(\"Persona\");\n    /** @type {boolean} */\n    odoobotOnboarding;\n    users = {};\n    /** @type {number} */\n    internalUserGroupId;\n    /** @type {number} */\n    mt_comment_id;\n    /** @type {boolean} */\n    hasMessageTranslationFeature;\n    imStatusTrackedPersonas = Record.many(\"Persona\", {\n        inverse: \"storeAsTrackedImStatus\",\n    });\n    hasLinkPreviewFeature = true;\n    // messaging menu\n    menu = { counter: 0 };\n    chatHub = Record.one(\"ChatHub\", { compute: () => ({}) });\n    failures = Record.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 = Record.one(\"Settings\");\n    openInviteThread = Record.one(\"Thread\");\n\n    fetchDeferred = new Deferred();\n    fetchParams = {};\n    fetchReadonly = true;\n    fetchSilent = true;\n\n    cannedReponses = this.makeCachedFetchData({ canned_responses: true });\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    get initMessagingParams() {\n        return {\n            init_messaging: {},\n        };\n    }\n\n    messagePostMutex = new Mutex();\n\n    menuThreads = Record.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 !== \"main\") {\n                threads = threads.filter(({ channel_type }) =>\n                    this.tabToThreadType(tab).includes(channel_type)\n                );\n            } else if (tab === \"main\" && this.env.inDiscussApp) {\n                threads = threads.filter(({ channel_type }) =>\n                    this.tabToThreadType(\"mailbox\").includes(channel_type)\n                );\n            }\n            return threads;\n        },\n        /**\n         * @this {import(\"models\").Store}\n         * @param {import(\"models\").Thread} a\n         * @param {import(\"models\").Thread} b\n         */\n        sort(a, b) {\n            /**\n             * Ordering:\n             * - threads with needaction\n             * - unread channels\n             * - read channels\n             * - odoobot chat\n             *\n             * In each group, thread with most recent message comes first\n             */\n            const aOdooBot = a.isCorrespondentOdooBot;\n            const bOdooBot = b.isCorrespondentOdooBot;\n            if (aOdooBot && !bOdooBot) {\n                return 1;\n            }\n            if (bOdooBot && !aOdooBot) {\n                return -1;\n            }\n            const aNeedaction = a.needactionMessages.length;\n            const bNeedaction = b.needactionMessages.length;\n            if (aNeedaction > 0 && bNeedaction === 0) {\n                return -1;\n            }\n            if (bNeedaction > 0 && aNeedaction === 0) {\n                return 1;\n            }\n            const aUnread = a.selfMember?.message_unread_counter;\n            const bUnread = b.selfMember?.message_unread_counter;\n            if (aUnread > 0 && bUnread === 0) {\n                return -1;\n            }\n            if (bUnread > 0 && aUnread === 0) {\n                return 1;\n            }\n            const aMessageDatetime = a.newestPersistentNotEmptyOfAllMessage?.datetime;\n            const bMessageDateTime = b.newestPersistentNotEmptyOfAllMessage?.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            return b.localId > a.localId ? 1 : -1;\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     * @returns {Deferred}\n     */\n    async fetchData(params, { readonly = true, silent = true } = {}) {\n        Object.assign(this.fetchParams, params);\n        this.fetchReadonly = this.fetchReadonly && readonly;\n        this.fetchSilent = this.fetchSilent && silent;\n        const fetchDeferred = this.fetchDeferred;\n        this._fetchDataDebounced();\n        return fetchDeferred;\n    }\n\n    /** Import data received from init_messaging */\n    async initialize() {\n        await this.fetchData(this.initMessagingParams, { readonly: false });\n        this.isReady.resolve();\n    }\n\n    /**\n     * Create a cacheable version of the `fetchData` 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 {{[key: string]: boolean}} params Parameters to pass to the `fetchData` method.\n     * @returns {{\n     *      fetch: () => ReturnType<Store[\"fetchData\"]>,\n     *      status: \"not_fetched\"|\"fetching\"|\"fetched\"\n     * }}\n     */\n    makeCachedFetchData(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.fetchData(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    async _fetchDataDebounced() {\n        const fetchDeferred = this.fetchDeferred;\n        this.fetchParams.context = {\n            ...user.context,\n            ...this.fetchParams.context,\n        };\n        rpc(this.fetchReadonly ? \"/mail/data\" : \"/mail/action\", this.fetchParams, {\n            silent: this.fetchSilent,\n        }).then(\n            (data) => {\n                const recordsByModel = this.insert(data, { html: true });\n                fetchDeferred.resolve(recordsByModel);\n            },\n            (error) => fetchDeferred.reject(error)\n        );\n        this.fetchDeferred = new Deferred();\n        this.fetchParams = {};\n        this.fetchReadonly = true;\n        this.fetchSilent = true;\n    }\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 pyModels = Object.values(pyToJsModels);\n        return Record.MAKE_UPDATE(function storeInsert() {\n            const res = {};\n            const recordsDataToDelete = [];\n            for (const [pyOrJsModelName, data] of Object.entries(dataByModelName)) {\n                if (pyModels.includes(pyOrJsModelName)) {\n                    console.warn(\n                        `store.insert() should receive the python model name instead of \u201c${pyOrJsModelName}\u201d.`\n                    );\n                }\n                const modelName = pyToJsModels[pyOrJsModelName] || 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 = addFieldsByPyModel[pyOrJsModelName];\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                const records = store[modelName].insert(insertData, options);\n                if (!res[modelName]) {\n                    res[modelName] = records;\n                } else {\n                    const knownRecordIds = new Set(res[modelName].map((r) => r.localId));\n                    res[modelName].push(...records.filter((r) => !knownRecordIds.has(r.localId)));\n                }\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            return res;\n        });\n    }\n\n    async startMeeting() {\n        const thread = await this.env.services[\"discuss.core.common\"].createGroupChat({\n            default_display_mode: \"video_full_screen\",\n            partners_to: [this.self.id],\n        });\n        this.ChatWindow.get(thread)?.update({ autofocus: 0 });\n        this.env.services[\"discuss.rtc\"].toggleCall(thread, { camera: true });\n        this.openInviteThread = thread;\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 model = ev.target.dataset.oeModel;\n        const id = Number(ev.target.dataset.oeId);\n        if (ev.target.closest(\".o_channel_redirect\") && model && id) {\n            ev.preventDefault();\n            this.Thread.getOrFetch({ model, id }).then((thread) => {\n                if (thread) {\n                    thread.open();\n                }\n            });\n            return true;\n        } else if (ev.target.closest(\".o_mail_redirect\") && id) {\n            ev.preventDefault();\n            this.openChat({ partnerId: id });\n            return true;\n        } else if (ev.target.tagName === \"A\" && model && id) {\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\n    onLinkFollowed(fromThread) {}\n\n    setup() {\n        super.setup();\n        this._fetchDataDebounced = debounce(\n            this._fetchDataDebounced,\n            Store.FETCH_DATA_DEBOUNCE_DELAY\n        );\n        this.updateBusSubscription = debounce(\n            () => this.env.services.bus_service.forceUpdateChannels(),\n            0\n        );\n    }\n\n    /** Provides an override point for when the store service has started. */\n    onStarted() {}\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        let chat = partner?.searchChat();\n        if (!chat || !chat.is_pinned) {\n            chat = await this.joinChat(partnerId || 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    /** @returns {number} */\n    getLastMessageId() {\n        return Object.values(this.Message.records).reduce(\n            (lastMessageId, message) => Math.max(lastMessageId, message.id),\n            0\n        );\n    }\n\n    getMentionsFromText(\n        body,\n        { mentionedChannels = [], mentionedPartners = [], specialMentions = [] } = {}\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(`@${partner.name}`)\n        );\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 { attachments, cannedResponseIds, isNote, mentionedChannels, mentionedPartners } =\n            postData;\n        const subtype = isNote ? \"mail.mt_note\" : \"mail.mt_comment\";\n        const validMentions = this.getMentionsFromText(body, {\n            mentionedChannels,\n            mentionedPartners,\n        });\n        const partner_ids = validMentions?.partners.map((partner) => partner.id) ?? [];\n        const recipientEmails = [];\n        const recipientAdditionalValues = {};\n        if (!isNote) {\n            const recipientIds = thread.suggestedRecipients\n                .filter((recipient) => recipient.persona && recipient.checked)\n                .map((recipient) => recipient.persona.id);\n            thread.suggestedRecipients\n                .filter((recipient) => recipient.checked && !recipient.persona)\n                .forEach((recipient) => {\n                    recipientEmails.push(recipient.email);\n                    recipientAdditionalValues[recipient.email] = recipient.create_values;\n                });\n            partner_ids.push(...recipientIds);\n        }\n        postData = {\n            body: await prettifyMessageContent(body, validMentions),\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        }\n        if (thread.model === \"discuss.channel\" && validMentions?.specialMentions.length) {\n            postData.special_mentions = validMentions.specialMentions;\n        }\n        const params = {\n            context: {\n                mail_post_autofollow: !isNote && thread.hasWriteAccess,\n            },\n            post_data: postData,\n            thread_id: thread.id,\n            thread_model: thread.model,\n        };\n        if (attachments.length) {\n            params.attachment_tokens = attachments.map((attachment) => attachment.access_token);\n        }\n        if (cannedResponseIds?.length) {\n            params.canned_response_ids = cannedResponseIds;\n        }\n        if (recipientEmails.length) {\n            Object.assign(params, {\n                partner_emails: recipientEmails,\n                partner_additional_values: recipientAdditionalValues,\n            });\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.Persona.insert({ id: partnerId, type: \"partner\" });\n            if (!partner.userId) {\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                partner.userId = userId;\n            }\n            return partner;\n        }\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 {[integer]}\n     */\n    getRecentChatPartnerIds() {\n        return Object.values(this.Thread.records)\n            .filter((thread) => thread.channel_type === \"chat\" && thread.correspondent)\n            .sort((a, b) => compareDatetime(b.lastInterestDt, a.lastInterestDt) || b.id - a.id)\n            .map((thread) => thread.correspondent.persona.id);\n    }\n\n    async joinChannel(id, name) {\n        await this.env.services.orm.call(\"discuss.channel\", \"add_members\", [[id]], {\n            partner_ids: [this.self.id],\n        });\n        const thread = this.Thread.insert({\n            channel_type: \"channel\",\n            id,\n            model: \"discuss.channel\",\n            name,\n        });\n        if (!thread.avatarCacheKey) {\n            thread.avatarCacheKey = \"hello\";\n        }\n        thread.open();\n        return thread;\n    }\n\n    async joinChat(id, forceOpen = false) {\n        const data = await this.env.services.orm.call(\"discuss.channel\", \"channel_get\", [], {\n            partners_to: [id],\n            force_open: forceOpen,\n        });\n        const { Thread } = this.store.insert(data);\n        return Thread[0];\n    }\n\n    async openChat(person) {\n        const chat = await this.getChat(person);\n        chat?.open();\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    openNewMessage() {\n        let cw = this.ChatWindow.get({ thread: undefined });\n        if (cw) {\n            cw.focus();\n            return;\n        }\n        cw = this.ChatWindow.insert({ thread: undefined, fromMessagingMenu: true });\n        this.chatHub.opened.unshift(cw);\n        cw.focus();\n    }\n\n    /**\n     * @param {string} searchTerm\n     * @param {Thread} thread\n     * @param {number|false} [before]\n     */\n    async search(searchTerm, thread, before = false) {\n        const { count, data, messages } = await rpc(thread.getFetchRoute(), {\n            ...thread.getFetchParams(),\n            search_term: await prettifyMessageContent(searchTerm), // formatted like message_post\n            before,\n        });\n        this.insert(data, { html: true });\n        return {\n            count,\n            loadMore: messages.length === this.FETCH_LIMIT,\n            messages: this.Message.insert(messages),\n        };\n    }\n\n    async searchPartners(searchStr = \"\", limit = 10) {\n        const partners = [];\n        const searchTerm = cleanTerm(searchStr);\n        for (const localId in this.Persona.records) {\n            const persona = this.Persona.records[localId];\n            if (persona.type !== \"partner\") {\n                continue;\n            }\n            const partner = persona;\n            if (\n                partner.name &&\n                cleanTerm(partner.name).includes(searchTerm) &&\n                ((partner.active && partner.userId) || partner === this.store.odoobot)\n            ) {\n                partners.push(partner);\n                if (partners.length >= limit) {\n                    break;\n                }\n            }\n        }\n        if (!partners.length) {\n            const data = await this.env.services.orm.silent.call(\"res.partner\", \"im_search\", [\n                searchTerm,\n                limit,\n            ]);\n            const { Persona = [] } = this.store.insert(data);\n            partners.push(...Persona);\n        }\n        return partners;\n    }\n}\nStore.register();\n\nexport const storeService = {\n    dependencies: [\"bus_service\", \"ui\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\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 ??= { id: -1, type: \"guest\" };\n        store.settings ??= {};\n        store.initialize();\n        store.onStarted();\n        return store;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.store\", storeService);\n", "import { useSequential } from \"@mail/utils/common/hooks\";\nimport { status, useComponent, useEffect, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nclass UseSuggestion {\n    constructor(comp) {\n        this.comp = comp;\n        useEffect(\n            (delimiter, position, term) => {\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.type !== \"partner\") {\n                    return; // guests cannot access fetch suggestion method\n                }\n                this.sequential(async () => {\n                    if (\n                        this.search.delimiter !== delimiter ||\n                        this.search.position !== position ||\n                        this.search.term !== term\n                    ) {\n                        return; // ignore obsolete call\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.state.isFetching = true;\n                    try {\n                        await this.suggestionService.fetchSuggestions(this.search, {\n                            thread: this.thread,\n                        });\n                    } catch {\n                        this.lastFetchedSearch = null;\n                    } finally {\n                        this.state.isFetching = false;\n                    }\n                    if (status(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 (\n                        this.search.delimiter === delimiter &&\n                        this.search.position === position &&\n                        this.search.term === term &&\n                        !this.state.items?.suggestions.length\n                    ) {\n                        this.clearSearch();\n                    }\n                });\n            },\n            () => {\n                return [this.search.delimiter, this.search.position, this.search.term];\n            }\n        );\n        useEffect(\n            () => {\n                this.detect();\n            },\n            () => [this.composer.selection.start, this.composer.selection.end, this.composer.text]\n        );\n    }\n    /** @type {import(\"@mail/core/common/composer\").Composer} */\n    comp;\n    get composer() {\n        return this.comp.props.composer;\n    }\n    sequential = useSequential();\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    }\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        const { start, end } = this.composer.selection;\n        const text = this.composer.text;\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(this.thread);\n        for (const candidatePosition of candidatePositions) {\n            if (candidatePosition < 0 || candidatePosition >= text.length) {\n                continue;\n            }\n            const candidateChar = text[candidatePosition];\n            if (\n                !supportedDelimiters.find(\n                    ([delimiter, allowedPosition]) =>\n                        delimiter === candidateChar &&\n                        (allowedPosition === undefined || allowedPosition === candidatePosition)\n                )\n            ) {\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: candidateChar,\n                position: candidatePosition,\n                term: text.substring(candidatePosition + 1, 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        const position = this.composer.selection.start;\n        const text = this.composer.text;\n        let before = text.substring(0, this.search.position + 1);\n        let after = text.substring(position, text.length);\n        if (this.search.delimiter === \":\") {\n            before = text.substring(0, this.search.position);\n            after = text.substring(position, text.length);\n        }\n        if (option.partner) {\n            this.composer.mentionedPartners.add({\n                id: option.partner.id,\n                type: \"partner\",\n            });\n        }\n        if (option.thread) {\n            this.composer.mentionedChannels.add({\n                model: \"discuss.channel\",\n                id: option.thread.id,\n            });\n        }\n        if (option.cannedResponse) {\n            this.composer.cannedResponses.push(option.cannedResponse);\n        }\n        this.clearSearch();\n        this.composer.text = before + option.label + \" \" + after;\n        this.composer.selection.start = before.length + option.label.length + 1;\n        this.composer.selection.end = before.length + option.label.length + 1;\n        this.composer.forceCursorMove = true;\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            sort: true,\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\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\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport class SuggestionService {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    constructor(env, services) {\n        this.env = env;\n        this.orm = services.orm;\n        this.store = services[\"mail.store\"];\n    }\n\n    getSupportedDelimiters(thread) {\n        return [[\"@\"], [\"#\"], [\":\"]];\n    }\n\n    async fetchSuggestions({ delimiter, term }, { thread } = {}) {\n        const cleanedSearchTerm = cleanTerm(term);\n        switch (delimiter) {\n            case \"@\": {\n                await this.fetchPartners(cleanedSearchTerm, thread);\n                break;\n            }\n            case \"#\":\n                await this.fetchThreads(cleanedSearchTerm);\n                break;\n            case \":\":\n                await this.store.cannedReponses.fetch();\n                break;\n        }\n    }\n\n    /**\n     * @param {string} term\n     * @param {import(\"models\").Thread} [thread]\n     */\n    async fetchPartners(term, thread) {\n        const kwargs = { search: term };\n        if (thread?.model === \"discuss.channel\") {\n            kwargs.channel_id = thread.id;\n        }\n        const data = await this.orm.silent.call(\n            \"res.partner\",\n            thread?.model === \"discuss.channel\"\n                ? \"get_mention_suggestions_from_channel\"\n                : \"get_mention_suggestions\",\n            [],\n            kwargs\n        );\n        this.store.insert(data);\n    }\n\n    /**\n     * @param {string} term\n     */\n    async fetchThreads(term) {\n        const suggestedThreads = await this.orm.silent.call(\n            \"discuss.channel\",\n            \"get_mention_suggestions\",\n            [],\n            { search: term }\n        );\n        this.store.Thread.insert(suggestedThreads);\n    }\n\n    searchCannedResponseSuggestions(cleanedSearchTerm, sort) {\n        const cannedResponses = Object.values(this.store[\"mail.canned.response\"].records).filter(\n            (cannedResponse) => {\n                return cleanTerm(cannedResponse.source).includes(cleanedSearchTerm);\n            }\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: sort ? cannedResponses.sort(sortFunc) : cannedResponses,\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, sort = false } = {}) {\n        thread = toRaw(thread);\n        const cleanedSearchTerm = cleanTerm(term);\n        switch (delimiter) {\n            case \"@\": {\n                return this.searchPartnerSuggestions(cleanedSearchTerm, thread, sort);\n            }\n            case \"#\":\n                return this.searchChannelSuggestions(cleanedSearchTerm, sort);\n            case \":\":\n                return this.searchCannedResponseSuggestions(cleanedSearchTerm, sort);\n        }\n        return {\n            type: undefined,\n            suggestions: [],\n        };\n    }\n\n    getPartnerSuggestions(thread) {\n        let partners;\n        const isNonPublicChannel =\n            thread &&\n            (thread.channel_type === \"group\" ||\n                thread.channel_type === \"chat\" ||\n                (thread.channel_type === \"channel\" && thread.authorizedGroupFullName));\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            partners = thread.channelMembers\n                .map((member) => member.persona)\n                .filter((persona) => persona.type === \"partner\");\n        } else {\n            partners = Object.values(this.store.Persona.records).filter((persona) => {\n                if (thread?.model !== \"discuss.channel\" && persona.eq(this.store.odoobot)) {\n                    return false;\n                }\n                return persona.type === \"partner\";\n            });\n        }\n        return partners;\n    }\n\n    searchPartnerSuggestions(cleanedSearchTerm, thread, sort) {\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: sort\n                ? [...this.sortPartnerSuggestions(suggestions, cleanedSearchTerm, thread)]\n                : suggestions,\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 = { recentChatPartnerIds: this.store.getRecentChatPartnerIds() };\n        const memberPartnerIds = new Set(\n            thread?.channelMembers\n                .filter((member) => member.persona.type === \"partner\")\n                .map((member) => member.persona.id)\n        );\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                    memberPartnerIds,\n                    searchTerms: cleanedSearchTerm,\n                    thread,\n                    context,\n                });\n                if (result !== undefined) {\n                    return result;\n                }\n            }\n        });\n    }\n\n    searchChannelSuggestions(cleanedSearchTerm, sort) {\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.authorizedGroupFullName;\n            const isPublicChannel2 = c2.channel_type === \"channel\" && !c2.authorizedGroupFullName;\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: sort ? suggestionList.sort(sortFunc) : suggestionList,\n        };\n    }\n}\n\nexport const suggestionService = {\n    dependencies: [\"orm\", \"mail.store\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} 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 { 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    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 { useBus, useRefListener, useService } from \"@web/core/utils/hooks\";\nimport { escape } from \"@web/core/utils/strings\";\n\nexport const PRESENT_VIEWPORT_THRESHOLD = 3;\nconst PRESENT_MESSAGE_THRESHOLD = 10;\n\n/**\n * @typedef {Object} Props\n * @property {boolean} [isInChatWindow=false]\n * @property {number} [jumpPresent=0]\n * @property {import(\"@mail/utils/common/hooks\").MessageEdition} [messageEdition]\n * @property {import(\"@mail/utils/common/hooks\").MessageToReplyTo} [messageToReplyTo]\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, Transition, DateSection };\n    static props = [\n        \"showDates?\",\n        \"isInChatWindow?\",\n        \"jumpPresent?\",\n        \"thread\",\n        \"messageEdition?\",\n        \"messageToReplyTo?\",\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    setup() {\n        super.setup();\n        this.escape = escape;\n        this.registerMessageRef = this.registerMessageRef.bind(this);\n        this.store = useState(useService(\"mail.store\"));\n        this.state = useState({\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').useMessageHighlight>|null} */\n        this.messageHighlight = this.env.messageHighlight\n            ? useState(this.env.messageHighlight)\n            : null;\n        this.scrollingToHighlight = false;\n        this.refByMessageId = reactive(new Map(), () => this.scrollToHighlighted());\n        useEffect(\n            () => this.scrollToHighlighted(),\n            () => [this.messageHighlight?.highlightedMessageId]\n        );\n        this.present = useRef(\"load-newer\");\n        this.jumpPresentRef = useRef(\"jump-present\");\n        this.root = useRef(\"messages\");\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        useEffect(\n            (loadNewer, mountedAndLoaded, unreadSynced) => {\n                if (\n                    loadNewer ||\n                    unreadSynced || // just marked as unread (local and server state are synced)\n                    !mountedAndLoaded ||\n                    !this.props.thread.selfMember ||\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.selfMember.hideUnreadBanner = true;\n                }\n            },\n            () => [\n                this.props.thread.loadNewer,\n                this.state.mountedAndLoaded,\n                this.props.thread.selfMember?.unreadSynced,\n                this.state.scrollTop,\n            ]\n        );\n        this.loadOlderState = useVisible(\n            \"load-older\",\n            async () => {\n                await this.messageHighlight?.scrollPromise;\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 this.messageHighlight?.scrollPromise;\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            () => {\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                        ? 0\n                        : height - pt - pb - (this.env.inChatter?.aside ? 75 : 0)\n                }px)`;\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.messageHighlight?.clearHighlight();\n                    if (this.props.thread.loadNewer) {\n                        this.jumpToPresent();\n                    } else {\n                        if (this.props.order === \"desc\") {\n                            this.scrollableRef.el.scrollTop = 0;\n                        } else {\n                            this.scrollableRef.el.scrollTop =\n                                this.scrollableRef.el.scrollHeight -\n                                this.scrollableRef.el.clientHeight;\n                        }\n                        this.props.thread.scrollTop = \"bottom\";\n                    }\n                    this.lastJumpPresent = this.props.jumpPresent;\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                if (this.props.thread.selfMember && this.props.thread.scrollUnread) {\n                    toRaw(this.props.thread).loadAround(\n                        this.props.thread.selfMember.new_message_separator\n                    );\n                } else {\n                    toRaw(this.props.thread).fetchNewMessages();\n                }\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        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    /**\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        const ref = this.scrollableRef;\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        let lastSetValue;\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        let 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        let snapshot;\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        let newestPersistentMessage;\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        let oldestPersistentMessage;\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        let loadNewer;\n        const reset = () => {\n            this.state.mountedAndLoaded = false;\n            this.loadOlderState.ready = false;\n            this.loadNewerState.ready = false;\n            lastSetValue = undefined;\n            snapshot = undefined;\n            newestPersistentMessage = undefined;\n            oldestPersistentMessage = undefined;\n            loadedAndPatched = false;\n            loadNewer = false;\n        };\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                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                        reset();\n                    }\n                });\n            }\n        });\n        onWillDestroy(() => stopOnChange());\n        const saveScroll = () => {\n            const thread = toRaw(this.props.thread);\n            const isBottom =\n                this.props.order === \"asc\"\n                    ? ref.el.scrollHeight - ref.el.scrollTop - ref.el.clientHeight < 30\n                    : ref.el.scrollTop < 30;\n            if (isBottom) {\n                thread.scrollTop = \"bottom\";\n            } else {\n                thread.scrollTop =\n                    this.props.order === \"asc\"\n                        ? ref.el.scrollTop\n                        : ref.el.scrollHeight - ref.el.scrollTop - ref.el.clientHeight;\n            }\n        };\n        const setScroll = (value) => {\n            ref.el.scrollTop = value;\n            lastSetValue = value;\n            saveScroll();\n        };\n        const applyScroll = () => {\n            if (!this.props.thread.isLoaded || !this.state.mountedAndLoaded) {\n                reset();\n                return;\n            }\n            // Use toRaw() to prevent scroll check from triggering renders.\n            const thread = toRaw(this.props.thread);\n            const olderMessages = thread.oldestPersistentMessage?.id < oldestPersistentMessage?.id;\n            const newerMessages = thread.newestPersistentMessage?.id > 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                    (loadNewer || thread.scrollTop !== \"bottom\"));\n            if (thread.selfMember && 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                    setScroll(messageCenter);\n                } else {\n                    const scrollTop =\n                        this.props.order === \"asc\"\n                            ? this.scrollableRef.el.scrollHeight -\n                              this.scrollableRef.el.clientHeight\n                            : 0;\n                    setScroll(scrollTop);\n                }\n                thread.scrollUnread = false;\n            } else if (snapshot && messagesAtTop) {\n                setScroll(snapshot.scrollTop + ref.el.scrollHeight - snapshot.scrollHeight);\n            } else if (snapshot && messagesAtBottom) {\n                setScroll(snapshot.scrollTop);\n            } else if (\n                !this.env.messageHighlight?.highlightedMessageId &&\n                thread.scrollTop !== undefined\n            ) {\n                let value;\n                if (thread.scrollTop === \"bottom\") {\n                    value =\n                        this.props.order === \"asc\" ? ref.el.scrollHeight - ref.el.clientHeight : 0;\n                } else {\n                    value =\n                        this.props.order === \"asc\"\n                            ? thread.scrollTop\n                            : ref.el.scrollHeight - thread.scrollTop - ref.el.clientHeight;\n                }\n                if (lastSetValue === undefined || Math.abs(lastSetValue - value) > 1) {\n                    setScroll(value);\n                }\n            }\n            snapshot = undefined;\n            newestPersistentMessage = thread.newestPersistentMessage;\n            oldestPersistentMessage = thread.oldestPersistentMessage;\n            loadNewer = thread.loadNewer;\n            if (!loadedAndPatched) {\n                loadedAndPatched = true;\n                this.loadOlderState.ready = true;\n                this.loadNewerState.ready = true;\n            }\n        };\n        onWillPatch(() => {\n            if (!loadedAndPatched) {\n                return;\n            }\n            snapshot = {\n                scrollHeight: ref.el.scrollHeight,\n                scrollTop: ref.el.scrollTop,\n            };\n        });\n        useEffect(applyScroll);\n        useChildSubEnv({\n            onImageLoaded: applyScroll,\n        });\n        const observer = new ResizeObserver(applyScroll);\n        useEffect(\n            (el, mountedAndLoaded) => {\n                if (el && mountedAndLoaded) {\n                    el.addEventListener(\"scroll\", saveScroll);\n                    observer.observe(el);\n                    return () => {\n                        observer.unobserve(el);\n                        el.removeEventListener(\"scroll\", saveScroll);\n                    };\n                }\n            },\n            () => [ref.el, this.state.mountedAndLoaded]\n        );\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 viewportHeight = (this.getViewportEl?.clientHeight ?? 0) * PRESENT_VIEWPORT_THRESHOLD;\n        const messagesHeight = [...this.props.thread.nonEmptyMessages]\n            .reverse()\n            .slice(0, PRESENT_MESSAGE_THRESHOLD)\n            .map((message) => this.refByMessageId.get(message.id))\n            .reduce((totalHeight, message) => totalHeight + (message?.el?.clientHeight ?? 0), 0);\n        const threshold = Math.max(viewportHeight, messagesHeight);\n        return this.state.showJumpPresent ? threshold - 200 : threshold;\n    }\n\n    get newMessageBannerText() {\n        if (this.props.thread.selfMember?.totalUnreadMessageCounter > 1) {\n            return _t(\"%s new messages\", this.props.thread.selfMember.totalUnreadMessageCounter);\n        }\n        return _t(\"1 new message\");\n    }\n\n    get preferenceButtonText() {\n        const [, before, inside, after] =\n            _t(\n                \"<button>Change your preferences</button> to receive new notifications in your inbox.\"\n            ).match(/(.*)<button>(.*)<\\/button>(.*)/) ?? [];\n        return { before, inside, after };\n    }\n\n    updateShowJumpPresent() {\n        this.state.showJumpPresent =\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.userId;\n        this.env.services.action.doAction(actionDescription);\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() {\n        this.messageHighlight?.clearHighlight();\n        await this.props.thread.loadAround();\n        this.props.thread.loadNewer = false;\n        this.props.thread.scrollTop = \"bottom\";\n        this.state.showJumpPresent = false;\n    }\n\n    async onClickUnreadMessagesBanner() {\n        await this.props.thread.loadAround(this.props.thread.selfMember.localNewMessageSeparator);\n        this.messageHighlight?.highlightMessage(\n            this.props.thread.firstUnreadMessage,\n            this.props.thread\n        );\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    isSquashed(msg, prevMsg) {\n        if (this.props.thread.model === \"mail.box\") {\n            return false;\n        }\n        if (\n            !prevMsg ||\n            prevMsg.message_type === \"notification\" ||\n            prevMsg.isEmpty ||\n            this.env.inChatter\n        ) {\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        return msg.datetime.ts - prevMsg.datetime.ts < 5 * 60 * 1000;\n    }\n\n    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            this.messageHighlight.scrollTo(el).then(() => (this.scrollingToHighlight = false));\n        }\n    }\n\n    get orderedMessages() {\n        return this.props.order === \"asc\"\n            ? [...this.props.thread.nonEmptyMessages]\n            : [...this.props.thread.nonEmptyMessages].reverse();\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\";\n\nexport const threadActionsRegistry = registry.category(\"mail.thread/actions\");\n\nthreadActionsRegistry\n    .add(\"fold-chat-window\", {\n        condition(component) {\n            return (\n                !component.ui.isSmall &&\n                component.props.chatWindow &&\n                component.props.chatWindow.thread\n            );\n        },\n        icon: \"fa fa-fw fa-minus\",\n        name(component) {\n            return !component.props.chatWindow?.isOpen ? _t(\"Open\") : _t(\"Fold\");\n        },\n        open(component) {\n            component.toggleFold();\n        },\n        displayActive(component) {\n            return !component.props.chatWindow?.isOpen;\n        },\n        sequence: 99,\n        sequenceQuick: 20,\n    })\n    .add(\"rename-thread\", {\n        condition(component) {\n            return (\n                component.thread &&\n                component.props.chatWindow?.isOpen &&\n                (component.thread.is_editable || component.thread.channel_type === \"chat\")\n            );\n        },\n        icon: \"fa fa-fw fa-pencil\",\n        name: _t(\"Rename Thread\"),\n        open(component) {\n            component.state.editingName = true;\n        },\n        sequence: 30,\n        sequenceGroup: 20,\n    })\n    .add(\"close\", {\n        condition(component) {\n            return component.props.chatWindow;\n        },\n        icon: \"oi fa-fw oi-close\",\n        name: _t(\"Close Chat Window (ESC)\"),\n        open(component) {\n            component.close();\n        },\n        sequence: 100,\n        sequenceQuick: 10,\n    })\n    .add(\"search-messages\", {\n        component: SearchMessagesPanel,\n        condition(component) {\n            return (\n                [\"discuss.channel\", \"mail.box\"].includes(component.thread?.model) &&\n                (!component.props.chatWindow || component.props.chatWindow.isOpen)\n            );\n        },\n        panelOuterClass: \"o-mail-SearchMessagesPanel bg-inherit\",\n        icon: \"oi oi-fw oi-search\",\n        iconLarge: \"oi oi-fw fa-lg oi-search\",\n        name: _t(\"Search Messages\"),\n        nameActive: _t(\"Close Search\"),\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        },\n        toggle: true,\n    });\n\nfunction transformAction(component, id, action) {\n    return {\n        /** Closes this action. */\n        close() {\n            if (this.toggle) {\n                component.threadActions.activeAction = component.threadActions.actionStack.pop();\n            }\n            action.close?.(component, this);\n        },\n        /** Optional component that should be displayed in the view when this action is active. */\n        component: action.component,\n        /** Condition to display the component of this action. */\n        get componentCondition() {\n            return this.isActive && this.component && this.condition && !this.popover;\n        },\n        /** Props to pass to the component of this action. */\n        get componentProps() {\n            return action.componentProps?.(this, component);\n        },\n        /** Condition to display this action. */\n        get condition() {\n            return action.condition(component);\n        },\n        /** Condition to disable the button of this action (but still display it). */\n        get disabledCondition() {\n            return action.disabledCondition?.(component);\n        },\n        /** Icon for the button this action. */\n        get icon() {\n            return typeof action.icon === \"function\" ? action.icon(component) : action.icon;\n        },\n        /** Large icon for the button this action. */\n        get iconLarge() {\n            return typeof action.iconLarge === \"function\"\n                ? action.iconLarge(component)\n                : action.iconLarge ?? action.icon;\n        },\n        /** Unique id of this action. */\n        id,\n        /** States whether this action is currently active. */\n        get isActive() {\n            return id === component.threadActions.activeAction?.id;\n        },\n        /** Name of this action, displayed to the user. */\n        get name() {\n            const res = this.isActive && action.nameActive ? action.nameActive : action.name;\n            return typeof res === \"function\" ? res(component) : res;\n        },\n        /**\n         * Action to execute when this action is selected (on or off).\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        onSelect({ keepPrevious } = {}) {\n            if (this.toggle && this.isActive) {\n                this.close();\n            } else {\n                this.open({ keepPrevious });\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 (component.threadActions.activeAction && keepPrevious) {\n                    component.threadActions.actionStack.push(component.threadActions.activeAction);\n                }\n                component.threadActions.activeAction = this;\n            }\n            action.open?.(component, this);\n        },\n        get panelOuterClass() {\n            return typeof action.panelOuterClass === \"function\"\n                ? action.panelOuterClass(component)\n                : action.panelOuterClass;\n        },\n        /** Determines whether this is a popover linked to this action. */\n        popover: null,\n        /** Determines the order of this action (smaller first). */\n        get sequence() {\n            return typeof action.sequence === \"function\"\n                ? action.sequence(component)\n                : action.sequence;\n        },\n        get sequenceGroup() {\n            return typeof action.sequenceGroup === \"function\"\n                ? action.sequenceGroup(component)\n                : action.sequenceGroup;\n        },\n        get sequenceQuick() {\n            return typeof action.sequenceQuick === \"function\"\n                ? action.sequenceQuick(component)\n                : action.sequenceQuick;\n        },\n        /** Component setup to execute when this action is registered. */\n        setup: action.setup,\n        /** Text for the button of this action */\n        text: action.text,\n        /** Determines whether this action is a one time effect or can be toggled (on or off). */\n        toggle: action.toggle,\n    };\n}\n\nexport function useThreadActions() {\n    const component = useComponent();\n    const transformedActions = threadActionsRegistry\n        .getEntries()\n        .map(([id, action]) => transformAction(component, id, action));\n    for (const action of transformedActions) {\n        if (action.setup) {\n            action.setup(action);\n        }\n    }\n    const state = useState({\n        get actions() {\n            return transformedActions\n                .filter((action) => action.condition)\n                .sort((a1, a2) => a1.sequence - a2.sequence);\n        },\n        get partition() {\n            const actions = 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        actionStack: [],\n        activeAction: null,\n    });\n    return state;\n}\n", "import { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { Thread } from \"./thread_model\";\nimport { _t } from \"@web/core/l10n/translation\";\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 props = {\n        thread: { type: Thread },\n        size: { optional: true, validate: (size) => [\"small\", \"medium\", \"large\"].includes(size) },\n        className: { type: String, optional: true },\n    };\n    static defaultProps = {\n        size: \"medium\",\n        className: \"\",\n    };\n\n    setup() {\n        super.setup();\n        this.store = useState(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\",\n            title: _t(\"No IM status available\"),\n        };\n    }\n}\n", "import { AND, Record } from \"@mail/core/common/record\";\nimport { prettifyMessageContent } from \"@mail/utils/common/format\";\nimport { assignDefined, compareDatetime, nearestGreaterThanOrEqual } from \"@mail/utils/common/misc\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatList } from \"@web/core/l10n/utils\";\nimport { user } from \"@web/core/user\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\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 * @property {boolean} checked\n */\n\nexport class Thread extends Record {\n    static id = AND(\"model\", \"id\");\n    /** @type {Object.<string, import(\"models\").Thread>} */\n    static records = {};\n    /** @returns {import(\"models\").Thread} */\n    static get(data) {\n        return super.get(data);\n    }\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    /** @returns {import(\"models\").Thread|import(\"models\").Thread[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n    static new() {\n        const thread = super.new(...arguments);\n        Record.onChange(thread, [\"state\"], () => {\n            if (thread.state === \"open\" && !this.store.env.services.ui.isSmall) {\n                const cw = this.store.ChatWindow?.insert({ thread });\n                thread.store.chatHub.opened.delete(cw);\n                thread.store.chatHub.opened.unshift(cw);\n            }\n            if (thread.state === \"folded\") {\n                const cw = this.store.ChatWindow?.insert({ thread });\n                thread.store.chatHub.folded.delete(cw);\n                thread.store.chatHub.folded.unshift(cw);\n            }\n        });\n        return thread;\n    }\n    static async getOrFetch(data) {\n        return this.get(data);\n    }\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    uuid;\n    /** @type {string} */\n    model;\n    allMessages = Record.many(\"Message\", {\n        inverse: \"thread\",\n    });\n    /** @type {boolean} */\n    areAttachmentsLoaded = false;\n    attachments = Record.many(\"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 canLeave() {\n        return (\n            [\"channel\", \"group\"].includes(this.channel_type) &&\n            !this.message_needaction_counter &&\n            !this.group_based_subscription &&\n            this.store.self?.type === \"partner\"\n        );\n    }\n    get canUnpin() {\n        return this.channel_type === \"chat\" && this.importantCounter === 0;\n    }\n    channelMembers = Record.many(\"ChannelMember\", {\n        inverse: \"thread\",\n        onDelete: (r) => r.delete(),\n        sort: (m1, m2) => m1.id - m2.id,\n    });\n    /**\n     * To be overridden.\n     * The purpose is to exclude technical channelMembers like bots and avoid\n     * \"wrong\" seen message indicator\n     */\n    get membersThatCanSeen() {\n        return this.channelMembers;\n    }\n    typingMembers = Record.many(\"ChannelMember\", { inverse: \"threadAsTyping\" });\n    otherTypingMembers = Record.many(\"ChannelMember\", {\n        /** @this {import(\"models\").Thread} */\n        compute() {\n            return this.typingMembers.filter((member) => !member.persona?.eq(this.store.self));\n        },\n    });\n    hasOtherMembersTyping = Record.attr(false, {\n        /** @this {import(\"models\").Thread} */\n        compute() {\n            return this.otherTypingMembers.length > 0;\n        },\n    });\n    toggleBusSubscription = Record.attr(false, {\n        compute() {\n            return (\n                this.model === \"discuss.channel\" &&\n                this.selfMember?.memberSince >= this.store.env.services.bus_service.startedAt\n            );\n        },\n        onUpdate() {\n            this.store.updateBusSubscription();\n        },\n    });\n    invitedMembers = Record.many(\"ChannelMember\");\n    composer = Record.one(\"Composer\", {\n        compute: () => ({}),\n        inverse: \"thread\",\n        onDelete: (r) => r.delete(),\n    });\n    correspondent = Record.one(\"ChannelMember\", {\n        compute() {\n            return this.computeCorrespondent();\n        },\n    });\n    correspondentCountry = Record.one(\"Country\", {\n        /** @this {import(\"models\").Thread} */\n        compute() {\n            return this.correspondent?.persona?.country ?? this.anonymous_country;\n        },\n    });\n    get showCorrespondentCountry() {\n        return (\n            this.channel_type === \"livechat\" &&\n            this.operator?.eq(this.store.self) &&\n            Boolean(this.correspondentCountry)\n        );\n    }\n    counter = 0;\n    counter_bus_id = 0;\n    /** @type {string} */\n    custom_channel_name;\n    /** @type {string} */\n    description;\n    displayToSelf = Record.attr(false, {\n        compute() {\n            return (\n                this.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 = Record.many(\"Follower\", {\n        /** @this {import(\"models\").Thread} */\n        onAdd(r) {\n            r.thread = this;\n        },\n        onDelete: (r) => r.delete(),\n    });\n    selfFollower = Record.one(\"Follower\", {\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        if (this.isChatChannel && this.selfMember?.message_unread_counter) {\n            return this.selfMember.totalUnreadMessageCounter;\n        }\n        return this.message_needaction_counter;\n    }\n    isCorrespondentOdooBot = Record.attr(undefined, {\n        compute() {\n            return this.correspondent?.persona.eq(this.store.odoobot);\n        },\n    });\n    isDisplayed = Record.attr(false, {\n        compute() {\n            return this.computeIsDisplayed();\n        },\n        onUpdate() {\n            if (this.selfMember && !this.isDisplayed) {\n                this.selfMember.syncUnread = true;\n            }\n        },\n    });\n    isLoadingAttachments = false;\n    isLoadedDeferred = new Deferred();\n    isLoaded = Record.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    is_pinned = Record.attr(undefined, {\n        /** @this {import(\"models\").Thread} */\n        onUpdate() {\n            this.onPinStateUpdated();\n        },\n    });\n    mainAttachment = Record.one(\"Attachment\");\n    memberCount = 0;\n    message_needaction_counter = 0;\n    message_needaction_counter_bus_id = 0;\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 = Record.many(\"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 = Record.many(\"Message\");\n    needactionMessages = Record.many(\"Message\", {\n        inverse: \"threadAsNeedaction\",\n        sort: (message1, message2) => message1.id - message2.id,\n    });\n    /** @type {string} */\n    name;\n    selfMember = Record.one(\"ChannelMember\", {\n        inverse: \"threadAsSelf\",\n    });\n    /** @type {'open' | 'folded' | 'closed'} */\n    state;\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 = Record.many(\"Message\");\n    /** @type {string} */\n    defaultDisplayMode;\n    scrollUnread = true;\n    suggestedRecipients = Record.attr([], {\n        onUpdate() {\n            for (const recipient of this.suggestedRecipients) {\n                if (recipient.checked === undefined) {\n                    recipient.checked = true;\n                }\n                recipient.persona = recipient.partner_id\n                    ? { type: \"partner\", id: recipient.partner_id }\n                    : false;\n            }\n        },\n    });\n    hasLoadingFailed = false;\n    canPostOnReadonly;\n    /** @type {luxon.DateTime} */\n    last_interest_dt = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {luxon.DateTime} */\n    lastInterestDt = Record.attr(undefined, {\n        type: \"datetime\",\n        compute() {\n            const selfMemberLastInterestDt = this.selfMember?.last_interest_dt;\n            const lastInterestDt = this.last_interest_dt;\n            return compareDatetime(selfMemberLastInterestDt, lastInterestDt) > 0\n                ? selfMemberLastInterestDt\n                : lastInterestDt;\n        },\n    });\n    /** @type {Boolean} */\n    is_editable;\n    /**\n     * This field is used for channels only.\n     * false means using the custom_notifications from user settings.\n     *\n     * @type {false|\"all\"|\"mentions\"|\"no_notif\"}\n     */\n    custom_notifications = false;\n    /** @type {luxon.DateTime} */\n    mute_until_dt = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {Boolean} */\n    isLocallyPinned = Record.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 = Record.one(\"Message\", {\n        onAdd(msg) {\n            msg.thread = this;\n        },\n    });\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.authorizedGroupFullName) {\n            return false;\n        }\n        return _t('Access restricted to group \"%(groupFullName)s\"', {\n            groupFullName: this.authorizedGroupFullName,\n        });\n    }\n\n    get areAllMembersLoaded() {\n        return this.memberCount === this.channelMembers.length;\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) => {\n            return a2.id - a1.id;\n        });\n        return attachments;\n    }\n\n    get isUnread() {\n        return this.selfMember?.message_unread_counter > 0 || this.needactionMessages.length > 0;\n    }\n\n    get isMuted() {\n        return this.mute_until_dt || this.store.settings.mute_until_dt;\n    }\n\n    get typesAllowingCalls() {\n        return [\"chat\", \"channel\", \"group\"];\n    }\n\n    get allowCalls() {\n        return (\n            this.typesAllowingCalls.includes(this.channel_type) &&\n            !this.correspondent?.persona.eq(this.store.odoobot)\n        );\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 displayName() {\n        if (this.channel_type === \"chat\" && this.correspondent) {\n            return this.custom_channel_name || this.correspondent.persona.name;\n        }\n        if (this.channel_type === \"group\" && !this.name) {\n            return formatList(\n                this.channelMembers.map((channelMember) => channelMember.persona.name)\n            );\n        }\n        return this.name;\n    }\n\n    get correspondents() {\n        return this.channelMembers.filter(({ persona }) => persona.notEq(this.store.self));\n    }\n\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.channelMembers.length === 1) {\n            // Self-chat.\n            return this.channelMembers[0];\n        }\n        return undefined;\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 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.isChatChannel\n            ? this.selfMember?.message_unread_counter ?? 0\n            : this.message_needaction_counter;\n    }\n\n    newestMessage = Record.one(\"Message\", {\n        inverse: \"threadAsNewest\",\n        compute() {\n            return this.messages.findLast((msg) => !msg.isEmpty);\n        },\n    });\n\n    firstUnreadMessage = Record.one(\"Message\", {\n        /** @this {import(\"models\").Thread} */\n        compute() {\n            if (!this.selfMember) {\n                return null;\n            }\n            const messages = this.nonEmptyMessages;\n            const separator = this.selfMember.localNewMessageSeparator;\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.Message.get({ id: separator });\n            if (!message || this.notEq(message.thread) || message.isEmpty) {\n                message = nearestGreaterThanOrEqual(messages, separator, (msg) => msg.id);\n            }\n            return message;\n        },\n    });\n\n    get newestPersistentMessage() {\n        return this.messages.findLast((msg) => Number.isInteger(msg.id));\n    }\n\n    newestPersistentAllMessages = Record.many(\"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 = Record.one(\"Message\", {\n        compute() {\n            return this.newestPersistentAllMessages[0];\n        },\n    });\n\n    newestPersistentNotEmptyOfAllMessage = Record.one(\"Message\", {\n        compute() {\n            return this.newestPersistentAllMessages.find((message) => !message.isEmpty);\n        },\n    });\n\n    get oldestPersistentMessage() {\n        return this.messages.find((msg) => Number.isInteger(msg.id));\n    }\n\n    onPinStateUpdated() {}\n\n    get hasSelfAsMember() {\n        return Boolean(this.selfMember);\n    }\n\n    hasSeenFeature = Record.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\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.some((message) => !message.isEmpty);\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);\n    }\n\n    get prefix() {\n        return this.isChatChannel ? \"@\" : \"#\";\n    }\n\n    get showUnreadBanner() {\n        return !this.selfMember?.hideUnreadBanner && this.selfMember?.localMessageUnreadCounter > 0;\n    }\n\n    get rpcParams() {\n        return {};\n    }\n\n    /** @type {undefined|number[]} */\n    lastMessageSeenByAllId = Record.attr(undefined, {\n        compute() {\n            if (!this.hasSeenFeature) {\n                return;\n            }\n            const otherMembers = this.channelMembers.filter((member) =>\n                member.persona.notEq(this.store.self)\n            );\n            if (otherMembers.length === 0) {\n                return;\n            }\n            const otherLastSeenMessageIds = otherMembers\n                .filter((member) => member.seen_message_id)\n                .map((member) => member.seen_message_id.id);\n            if (otherLastSeenMessageIds.length === 0) {\n                return;\n            }\n            return Math.min(...otherLastSeenMessageIds);\n        },\n    });\n\n    lastSelfMessageSeenByEveryone = Record.one(\"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\n    get unknownMembersCount() {\n        return this.memberCount - this.channelMembers.length;\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    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.channelMembers.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\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        try {\n            const { data, messages } = await this.fetchMessagesData({ after, around, before });\n            this.store.insert(data, { html: true });\n            this.isLoaded = true;\n            return this.store.Message.insert(messages.reverse());\n        } catch (e) {\n            this.hasLoadingFailed = true;\n            throw e;\n        } finally {\n            this.status = \"ready\";\n        }\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            limit: !around && around !== 0 ? this.store.FETCH_LIMIT : this.store.FETCH_LIMIT * 2,\n            after,\n            around,\n            before,\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        try {\n            const fetched = await this.fetchMessages({ after, before });\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(\n                (message) => !alreadyKnownMessages.has(message.id)\n            );\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        } catch {\n            // handled in fetchMessages\n        }\n        this.pendingNewMessages = [];\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        try {\n            const fetched = await this.fetchMessages({ after });\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        } catch {\n            // handled in fetchMessages\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    async leave() {\n        await this.store.env.services.orm.call(\"discuss.channel\", \"action_unfollow\", [this.id]);\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        try {\n            this.isLoaded = false;\n            this.scrollTop = undefined;\n            this.messages = await this.fetchMessages({ around: messageId });\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        } catch {\n            // handled in fetchMessages\n        }\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]\n     * @param {boolean} [options.sync] Whether to sync the unread message\n     * state with the server values.\n     */\n    markAsRead({ sync } = {}) {\n        const newestPersistentMessage = this.newestPersistentOfAllMessage;\n        if (!newestPersistentMessage && !this.isLoaded) {\n            this.isLoadedDeferred.then(() => new Promise(setTimeout)).then(() => this.markAsRead());\n        }\n        const alreadyReadBySelf = newestPersistentMessage?.isReadBySelf;\n        if (this.selfMember) {\n            this.selfMember.syncUnread = sync ?? this.selfMember.syncUnread;\n            this.selfMember.seen_message_id = newestPersistentMessage;\n        }\n        if (newestPersistentMessage && this.selfMember && !alreadyReadBySelf) {\n            rpc(\"/discuss/channel/mark_as_read\", {\n                channel_id: this.id,\n                last_message_id: newestPersistentMessage.id,\n                sync,\n            }).catch((e) => {\n                if (e.code !== 404) {\n                    throw e;\n                }\n            });\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 {Object} [options] */\n    open(options) {}\n\n    openChatWindow({ fromMessagingMenu } = {}) {\n        const cw = this.store.ChatWindow.insert(\n            assignDefined({ thread: this }, { fromMessagingMenu })\n        );\n        this.store.chatHub.opened.delete(cw);\n        this.store.chatHub.opened.unshift(cw);\n        if (!isMobileOS()) {\n            cw.focus();\n        }\n        this.state = \"open\";\n        cw.notifyState();\n        return cw;\n    }\n\n    pin() {\n        if (this.model !== \"discuss.channel\" || this.store.self.type !== \"partner\") {\n            return;\n        }\n        this.is_pinned = true;\n        return this.store.env.services.orm.silent.call(\n            \"discuss.channel\",\n            \"channel_pin\",\n            [this.id],\n            { pinned: true }\n        );\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\") ||\n                this.channel_type === \"chat\" ||\n                this.channel_type === \"group\")\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.channel_type === \"chat\") {\n                this.custom_channel_name = newName;\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) && message.author.eq(this.store.self)) {\n            this.messages.splice(this.messages.indexOf(tmpMsg), 1, message);\n            return;\n        }\n        this.messages.add(message);\n    }\n\n    /** @param {string} 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, mentionedChannels, mentionedPartners } = 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                attachments: attachments,\n                res_id: this.id,\n                model: \"discuss.channel\",\n            };\n            tmpData.author = this.store.self;\n            if (parentId) {\n                tmpData.parentMessage = this.store.Message.get(parentId);\n            }\n            const prettyContent = await prettifyMessageContent(\n                body,\n                this.store.getMentionsFromText(body, {\n                    mentionedChannels,\n                    mentionedPartners,\n                })\n            );\n            tmpMsg = this.store.Message.insert(\n                {\n                    ...tmpData,\n                    body: prettyContent,\n                    isPending: true,\n                    thread: this,\n                },\n                { html: true }\n            );\n            this.messages.push(tmpMsg);\n            if (this.selfMember) {\n                this.selfMember.syncUnread = true;\n                this.selfMember.seen_message_id = tmpMsg;\n                this.selfMember.new_message_separator = tmpMsg.id + 1;\n            }\n        }\n        const data = await this.store.doMessagePost(params, tmpMsg);\n        if (!data) {\n            return;\n        }\n        const { Message: messages = [] } = this.store.insert(data, { html: true });\n        const [message] = messages;\n        this.addOrReplaceMessage(message, tmpMsg);\n        if (this.selfMember?.seen_message_id?.id < message.id) {\n            this.selfMember.seen_message_id = message;\n            this.selfMember.new_message_separator = message.id + 1;\n        }\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.mainAttachment = this.attachmentsInWebClientView[index];\n        await this.store.env.services.orm.call(\"ir.attachment\", \"register_as_main_attachment\", [\n            this.mainAttachment.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\nThread.register();\n", "import { Record } from \"./record\";\n\nexport class Volume extends Record {\n    static id = \"persona\";\n\n    persona = Record.one(\"Persona\");\n    volume = 1;\n}\n\nVolume.register();\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Message } from \"@mail/core/common/message\";\nimport { onWillUnmount } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\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-read-more-less\")?.remove();\n        });\n    },\n\n    /**\n     * @override\n     * @param {HTMLElement} bodyEl\n     */\n    prepareMessageBody(bodyEl) {\n        super.prepareMessageBody(...arguments);\n        Array.from(bodyEl.querySelectorAll(\".o-mail-read-more-less\")).forEach((el) => el.remove());\n        this.insertReadMoreLess(bodyEl);\n    },\n\n    /**\n     * Modifies the message to add the 'read more/read less' 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 'read more/read less'.\n     *\n     * @param {HTMLElement} bodyEl\n     */\n    insertReadMoreLess(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 readMoreNodes;\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 (!readMoreNodes) {\n                    readMoreNodes = [];\n                    groups.push(readMoreNodes);\n                }\n                hide(childEl);\n                readMoreNodes.push(childEl);\n            } else {\n                readMoreNodes = undefined;\n                this.insertReadMoreLess(childEl);\n            }\n        }\n\n        for (const group of groups) {\n            const index = this.state.lastReadMoreIndex++;\n            // Insert link just before the first node\n            const readMoreLessEl = document.createElement(\"a\");\n            readMoreLessEl.className = \"o-mail-read-more-less d-block\";\n            readMoreLessEl.href = \"#\";\n            readMoreLessEl.textContent = _t(\"Read More\");\n            group[0].parentNode.insertBefore(readMoreLessEl, group[0]);\n\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                readMoreLessEl.textContent = isReadMore\n                    ? _t(\"Read More\").toString()\n                    : _t(\"Read Less\").toString();\n            };\n            readMoreLessEl.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", "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 { stateToUrl } from \"@web/core/browser/router\";\nimport { loadEmoji } from \"@web/core/emoji_picker/emoji_picker\";\n\nimport { escape, unaccent } from \"@web/core/utils/strings\";\n\nconst urlRegexp =\n    /\\b(?:https?:\\/\\/\\d{1,3}(?:\\.\\d{1,3}){3}|(?:https?:\\/\\/|(?:www\\.))[-a-z0-9@:%._+~#=\\u00C0-\\u024F\\u1E00-\\u1EFF]{2,256}\\.[a-z]{2,13})\\b(?:[-a-z0-9@:%_+~#?&[\\]^|{}`\\\\'$//=\\u00C0-\\u024F\\u1E00-\\u1EFF]|[.]*[-a-z0-9@:%_+~#?&[\\]^|{}`\\\\'$//=\\u00C0-\\u024F\\u1E00-\\u1EFF]|,(?!$| )|\\.(?!$| |\\.)|;(?!$| ))*/gi;\n\n/**\n * Escape < > & as html entities\n *\n * @param {string}\n * @return {string}\n */\nconst _escapeEntities = (function () {\n    const map = { \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\" };\n    const escaper = function (match) {\n        return map[match];\n    };\n    const testRegexp = RegExp(\"(?:&|<|>)\");\n    const replaceRegexp = RegExp(\"(?:&|<|>)\", \"g\");\n    return function (string) {\n        string = string == null ? \"\" : \"\" + string;\n        return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;\n    };\n})();\n\n/**\n * @param rawBody {string}\n * @param validRecords {Object}\n * @param validRecords.partners {Partner}\n */\nexport async function prettifyMessageContent(rawBody, validRecords = []) {\n    // Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url\n    // Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match.\n    // And further extended to include Latin-1 Supplement, Latin Extended-A, Latin Extended-B and Latin Extended Additional.\n    const escapedAndCompactContent = escapeAndCompactTextContent(rawBody);\n    let body = escapedAndCompactContent.replace(/&nbsp;/g, \" \").trim();\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, validRecords);\n    body = parseAndTransform(body, addLink);\n    body = await _generateEmojisOnHtml(body);\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} htmlString\n * @param {function} transformFunction\n * @returns {string}\n */\nexport function parseAndTransform(htmlString, transformFunction) {\n    const openToken = \"OPEN\" + Date.now();\n    const string = htmlString.replace(/&lt;/g, openToken);\n    let children;\n    try {\n        const div = document.createElement(\"div\");\n        div.innerHTML = string; // /!\\ quotes are unescaped\n        children = Array.from(div.childNodes);\n    } catch {\n        const div = document.createElement(\"div\");\n        div.innerHTML = `<pre>${string}</pre>`;\n        children = Array.from(div.childNodes);\n    }\n    return _parseAndTransform(children, transformFunction).replace(\n        new RegExp(openToken, \"g\"),\n        \"&lt;\"\n    );\n}\n\n/**\n * @param {Node[]} nodes\n * @param {function} transformFunction with:\n *   param node\n *   param function\n *   return string\n * @return {string}\n */\nfunction _parseAndTransform(nodes, transformFunction) {\n    if (!nodes) {\n        return;\n    }\n    return Object.values(nodes)\n        .map((node) => {\n            return transformFunction(node, function () {\n                return _parseAndTransform(node.childNodes, transformFunction);\n            });\n        })\n        .join(\"\");\n}\n\n/**\n * @param {string} text\n * @return {string} 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 += _escapeEntities(text.slice(curIndex, match.index));\n        // Decode the url first, in case it's already an encoded url\n        const url = decodeURI(match[0]);\n        const href = encodeURI(!/^https?:\\/\\//i.test(url) ? \"http://\" + url : url);\n        result += `<a target=\"_blank\" rel=\"noreferrer noopener\" href=\"${href}\">${_escapeEntities(\n            url\n        )}</a>`;\n        curIndex = match.index + match[0].length;\n    }\n    return result + _escapeEntities(text.slice(curIndex));\n}\n\nexport function addLink(node, transformChildren) {\n    if (node.nodeType === 3) {\n        // text node\n        const linkified = linkify(node.data);\n        if (linkified !== node.data) {\n            const div = document.createElement(\"div\");\n            div.innerHTML = 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 node.outerHTML;\n    }\n    transformChildren();\n    return node.outerHTML;\n}\n\n/**\n * Returns an escaped conversion of a content.\n *\n * @param {string} content\n * @returns {string}\n */\nexport function escapeAndCompactTextContent(content) {\n    //Removing unwanted extra spaces from message\n    let value = escape(content).trim();\n    value = value.replace(/(\\r|\\n){2,}/g, \"<br/><br/>\");\n    value = value.replace(/(\\r|\\n)/g, \"<br/>\");\n\n    // prevent html space collapsing\n    value = value.replace(/ /g, \"&nbsp;\").replace(/([^>])&nbsp;([^<])/g, \"$1 $2\");\n    return value;\n}\n\n/**\n * @param body {string}\n * @param validRecords {Object}\n * @param validRecords.partners {Array}\n * @return {string}\n */\nfunction generateMentionsLinks(body, { partners = [], threads = [], specialMentions = [] }) {\n    const mentions = [];\n    for (const partner of partners) {\n        const placeholder = `@-mention-partner-${partner.id}`;\n        const text = `@${escape(partner.name)}`;\n        mentions.push({\n            class: \"o_mail_redirect\",\n            id: partner.id,\n            model: \"res.partner\",\n            placeholder,\n            text,\n        });\n        body = body.replace(text, placeholder);\n    }\n    for (const thread of threads) {\n        const placeholder = `#-mention-channel-${thread.id}`;\n        let className, text;\n        if (thread.parent_channel_id) {\n            className = \"o_channel_redirect o_channel_redirect_asThread\";\n            text = escape(`#${thread.parent_channel_id.displayName} > ${thread.displayName}`);\n        } else {\n            className = \"o_channel_redirect\";\n            text = escape(`#${thread.displayName}`);\n        }\n        mentions.push({\n            class: className,\n            id: thread.id,\n            model: \"discuss.channel\",\n            placeholder,\n            text,\n        });\n        body = body.replace(text, placeholder);\n    }\n    for (const special of specialMentions) {\n        body = body.replace(\n            `@${escape(special)}`,\n            `<a href=\"#\" class=\"o-discuss-mention\">@${escape(special)}</a>`\n        );\n    }\n    for (const mention of mentions) {\n        const href = `href='${stateToUrl({ model: mention.model, resId: mention.id })}'`;\n        const attClass = `class='${mention.class}'`;\n        const dataOeId = `data-oe-id='${mention.id}'`;\n        const dataOeModel = `data-oe-model='${mention.model}'`;\n        const target = \"target='_blank'\";\n        const link = `<a ${href} ${attClass} ${dataOeId} ${dataOeModel} ${target} contenteditable=\"false\">${mention.text}</a>`;\n        body = body.replace(mention.placeholder, link);\n    }\n    return body;\n}\n\n/**\n * @private\n * @param {string} htmlString\n * @returns {string}\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 = String(source).replace(/([.*+?=^!:${}()|[\\]/\\\\])/g, \"\\\\$1\");\n            const regexp = new RegExp(\"(\\\\s|^)(\" + escapedSource + \")(?=\\\\s|$)\", \"g\");\n            htmlString = htmlString.replace(regexp, \"$1\" + emoji.codepoints);\n        }\n    }\n    return htmlString;\n}\n\nexport function htmlToTextContentInline(htmlString) {\n    const fragment = document.createDocumentFragment();\n    const div = document.createElement(\"div\");\n    fragment.appendChild(div);\n    htmlString = htmlString.replace(/<br\\s*\\/?>/gi, \" \");\n    try {\n        div.innerHTML = htmlString;\n    } catch {\n        div.innerHTML = `<pre>${htmlString}</pre>`;\n    }\n    return div.textContent\n        .trim()\n        .replace(/[\\n\\r]/g, \"\")\n        .replace(/\\s\\s+/g, \" \");\n}\n\nexport function convertBrToLineBreak(str) {\n    return new DOMParser().parseFromString(\n        str.replaceAll(\"<br>\", \"\\n\").replaceAll(\"</br>\", \"\\n\"),\n        \"text/html\"\n    ).body.textContent;\n}\n\nexport function cleanTerm(term) {\n    return unaccent((typeof term === \"string\" ? term : \"\").toLowerCase());\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", "import {\n    onMounted,\n    onPatched,\n    onWillUnmount,\n    useComponent,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder_owl\";\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 * @param {string | string[]} 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.\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 * @returns {({ isHover: boolean })}\n */\nexport function useHover(refNames, { onHover, onAway, onHovering } = {}) {\n    refNames = Array.isArray(refNames) ? refNames : [refNames];\n    const targets = [];\n    let wasHovering = false;\n    let hoveringTimeout;\n    let awayTimeout;\n    for (const refName of refNames) {\n        targets.push({\n            ref: refName.endsWith(\"*\")\n                ? useRef(refName.substring(0, refName.length - 1))\n                : useRef(refName),\n        });\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        _count: 0,\n        _isHover: false,\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                }, 200);\n            }\n        }\n        wasHovering = hovering;\n    }\n    function onmouseenter(ev) {\n        if (state.isHover) {\n            return;\n        }\n        for (const target of targets) {\n            if (!target.ref.el) {\n                continue;\n            }\n            if (target.ref.el.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 targets) {\n            if (!target.ref.el) {\n                continue;\n            }\n            if (target.ref.el.contains(ev.relatedTarget)) {\n                return;\n            }\n        }\n        setHover(false);\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    return state;\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} MessageHighlight\n * @property {function} clearHighlight\n * @property {function} highlightMessage\n * @property {number|null} highlightedMessageId\n * @returns {MessageHighlight}\n */\nexport function useMessageHighlight(duration = 2000) {\n    let timeout;\n    const state = useState({\n        clearHighlight() {\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.notEq(message.thread)) {\n                return;\n            }\n            await thread.loadAround(message.id);\n            const lastHighlightedMessageId = state.highlightedMessageId;\n            this.clearHighlight();\n            if (lastHighlightedMessageId === message.id) {\n                // Give some time for the state to update.\n                await new Promise(setTimeout);\n            }\n            thread.scrollTop = undefined;\n            state.highlightedMessageId = message.id;\n            timeout = browser.setTimeout(() => this.clearHighlight(), duration);\n        },\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 useSelection({ refName, model, preserveOnClickAwayPredicate = () => false }) {\n    const ui = useState(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 (!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 useMessageEdition() {\n    const state = useState({\n        /** @type {import('@mail/core/common/composer').Composer} */\n        composerOfThread: null,\n        /** @type {import('@mail/core/common/message_model').Message} */\n        editingMessage: null,\n        exitEditMode() {\n            state.editingMessage = null;\n            if (state.composerOfThread) {\n                state.composerOfThread.props.composer.autofocus++;\n            }\n        },\n    });\n    return state;\n}\n\n/**\n * @typedef {Object} MessageToReplyTo\n * @property {function} cancel\n * @property {function} isNotSelected\n * @property {function} isSelected\n * @property {import(\"models\").Message|null} message\n * @property {import(\"models\").Thread|null} thread\n * @property {function} toggle\n * @returns {MessageToReplyTo}\n */\nexport function useMessageToReplyTo() {\n    return useState({\n        cancel() {\n            Object.assign(this, { message: null, thread: null });\n        },\n        /**\n         * @param {import(\"models\").Thread} thread\n         * @param {import(\"models\").Message} message\n         * @returns {boolean}\n         */\n        isNotSelected(thread, message) {\n            return thread.eq(this.thread) && message.notEq(this.message);\n        },\n        /**\n         * @param {import(\"models\").Thread} thread\n         * @param {import(\"models\").Message} message\n         * @returns {boolean}\n         */\n        isSelected(thread, message) {\n            return thread.eq(this.thread) && message.eq(this.message);\n        },\n        /** @type {import(\"models\").Message|null} */\n        message: null,\n        /** @type {import(\"models\").Thread|null} */\n        thread: null,\n        /**\n         * @param {import(\"models\").Thread} thread\n         * @param {import(\"models\").Message} message\n         */\n        toggle(thread, message) {\n            if (message.eq(this.message)) {\n                this.cancel();\n            } else {\n                Object.assign(this, { message, thread });\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 = useState(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 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        const { height } = getRect(ctx.current.element);\n        ctx.current.container = document.createElement(\"div\");\n        addStyle(ctx.current.container, {\n            position: \"fixed\",\n            top: 0,\n            bottom: `${height}px`,\n            left: 0,\n            right: 0,\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 { top, left };\n    },\n});\n", "import { reactive } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\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 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", "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 = useState(useService(\"mail.store\"));\n        this.state = useState({\n            jumpThreadPresent: 0,\n            /** @type {import(\"models\").Thread} */\n            thread: undefined,\n            aside: false,\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            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?.fetchData) {\n                if (this.env.chatter) {\n                    this.env.chatter.fetchData = 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: this.store.self,\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    load(thread, requestList) {\n        if (!thread.id || !this.state.thread?.eq(thread)) {\n            return;\n        }\n        thread.fetchData(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?.fetchData) {\n            if (this.env.chatter) {\n                this.env.chatter.fetchData = 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 { rpc } from \"@web/core/network/rpc\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    /** @param {string[]} requestList */\n    async fetchData(requestList) {\n        if (requestList.includes(\"messages\")) {\n            this.fetchNewMessages();\n        }\n        const result = await rpc(\"/mail/thread/data\", {\n            request_list: requestList,\n            thread_id: this.id,\n            thread_model: this.model,\n            ...this.rpcParams,\n        });\n        this.store.insert(result, { html: true });\n    },\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 { Component, useState } from \"@odoo/owl\";\nimport { ResizablePanel } from \"@web/core/resizable_panel/resizable_panel\";\nimport { 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 = [\"icon?\", \"title?\", \"resizable?\", \"slots?\", \"initialWidth?\", \"minWidth?\"];\n    static defaultProps = { resizable: true };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n    }\n\n    get classNames() {\n        return `o-mail-ActionPanel overflow-auto d-flex flex-column flex-shrink-0 position-relative py-2 pt-0 h-100 bg-inherit ${\n            !this.env.inChatter ? \" px-2\" : \" o-mail-ActionPanel-chatter\"\n        } ${this.env.inDiscussApp ? \" o-mail-discussSidebarBgColor\" : \"\"}`;\n    }\n}\n", "import { Chatter } from \"@mail/chatter/web_portal/chatter\";\n\nChatter.template = \"portal.Chatter\";\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.env.inFrontendPortalChatter) {\n            return _t(\"Write a message\u2026\");\n        }\n        return super.placeholder;\n    },\n});\n", "import { Picker } from \"@mail/core/common/picker\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Picker.prototype, {\n    get popoverSettings() {\n        const settings = super.popoverSettings;\n        settings.fixedPosition = false;\n        return settings;\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    get fetchRouteChatter() {\n        return \"/mail/chatter_fetch\";\n    },\n});\n", "import { AttachmentUploadService } from \"@mail/core/common/attachment_upload_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(AttachmentUploadService.prototype, {\n    _buildFormData(formData, file, thread, composer, tmpId, options) {\n        super._buildFormData(...arguments);\n        if (thread.rpcParams.hash && thread.rpcParams.pid) {\n            formData.append(\"hash\", thread.rpcParams.hash);\n            formData.append(\"pid\", thread.rpcParams.pid);\n        }\n        if (thread.rpcParams.token) {\n            formData.append(\"token\", thread.rpcParams.token);\n        }\n        return formData;\n    },\n});\n", "import { Chatter } from \"@mail/chatter/web_portal/chatter\";\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { useRef, onWillPatch, useEffect } from \"@odoo/owl\";\n\npatch(Chatter.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.topRef = useRef(\"top\");\n        onWillPatch(() => {\n            // Keep the composer position under the page header on scrolling\n            if (!this.props.twoColumns) {\n                const paddingTop = document.querySelector(\"#wrapwrap header\")\n                    ? document.querySelector(\"#wrapwrap header\").getBoundingClientRect().height +\n                      15 +\n                      \"px\"\n                    : \"\";\n                this.observer = new window.IntersectionObserver(\n                    ([e]) =>\n                        (e.target.style.paddingTop =\n                            e.target.getBoundingClientRect().y < 1 ? paddingTop : \"20px\"),\n                    {\n                        threshold: [1],\n                    }\n                );\n            }\n        });\n        useEffect(\n            () => {\n                if (this.topRef.el) {\n                    this.observer?.observe(this.topRef.el);\n                }\n            },\n            () => [this.topRef.el]\n        );\n    },\n});\n", "import { Composer } from \"@mail/core/common/composer_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Composer.prototype, {\n    portalComment: false,\n});\n", "import { Composer } from \"@mail/core/common/composer\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Composer.prototype, {\n    setup() {\n        super.setup();\n        if (this.env.inFrontendPortalChatter) {\n            this.suggestion = undefined;\n        }\n    },\n\n    get showComposerAvatar() {\n        return super.showComposerAvatar || (this.compact && this.props.composer.portalComment);\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    get authorAvatarUrl() {\n        if (this.message.author_avatar_url) {\n            return this.message.author_avatar_url;\n        }\n        if (this.message.thread.access_token) {\n            return `/mail/avatar/mail.message/${this.message.id}/author_avatar/50x50?access_token=${this.message.thread.access_token}`;\n        }\n        return super.authorAvatarUrl;\n    },\n});\n", "import { Chatter } from \"@mail/chatter/web_portal/chatter\";\n\nimport { OverlayContainer } from \"@web/core/overlay/overlay_container\";\nimport { Component, xml, useSubEnv } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class PortalChatter extends Component {\n    static template = xml`\n        <Chatter threadId=\"props.resId\" threadModel=\"props.resModel\" composer=\"props.composer\" twoColumns=\"props.twoColumns\"/>\n        <div class=\"position-fixed\" style=\"z-index:1030\"><OverlayContainer overlays=\"overlayService.overlays\"/></div>\n    `;\n    static components = { Chatter, OverlayContainer };\n    static props = [\"resId\", \"resModel\", \"composer\", \"twoColumns\", \"displayRating\"];\n\n    setup() {\n        useSubEnv({\n            displayRating: this.props.displayRating,\n            inFrontendPortalChatter: true,\n        });\n        this.overlayService = useService(\"overlay\");\n    }\n}\n", "import { PortalChatter } from \"@portal/chatter/frontend/portal_chatter\";\nimport { App } from \"@odoo/owl\";\nimport { getBundle } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { session } from \"@web/session\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { getTemplate } from \"@web/core/templates\";\n\nexport class PortalChatterService {\n    constructor(env, services) {\n        this.setup(env, services);\n    }\n\n    setup(env, services) {\n        this.store = services[\"mail.store\"];\n        this.busService = services.bus_service;\n    }\n\n    async createShadow(root) {\n        const shadow = root.attachShadow({ mode: \"open\" });\n        const res = await getBundle(\"portal.assets_chatter_style\");\n        for (const url of res.cssLibs) {\n            const link = document.createElement(\"link\");\n            link.rel = \"stylesheet\";\n            link.href = url;\n            shadow.appendChild(link);\n            await new Promise((res, rej) => {\n                link.addEventListener(\"load\", res);\n                link.addEventListener(\"error\", rej);\n            });\n        }\n        return shadow;\n    }\n\n    async initialize(env) {\n        const chatterEl = document.querySelector(\".o_portal_chatter\");\n        const props = {\n            resId: parseInt(chatterEl.getAttribute(\"data-res_id\")),\n            resModel: chatterEl.getAttribute(\"data-res_model\"),\n            composer:\n                parseInt(chatterEl.getAttribute(\"data-allow_composer\")) &&\n                (chatterEl.getAttribute(\"data-token\") || !session.is_public),\n            twoColumns: chatterEl.getAttribute(\"data-two_columns\") === \"true\" ? true : false,\n            displayRating: chatterEl.getAttribute(\"data-display_rating\") === \"True\" ? true : false,\n        };\n        const root = document.createElement(\"div\");\n        root.setAttribute(\"id\", \"chatterRoot\");\n        if (props.twoColumns) {\n            root.classList.add(\"p-0\");\n        }\n        chatterEl.appendChild(root);\n        this.createShadow(root).then((shadow) => {\n            new App(PortalChatter, {\n                env,\n                getTemplate,\n                props,\n                translatableAttributes: [\"data-tooltip\"],\n                translateFn: _t,\n                dev: env.debug,\n            }).mount(shadow);\n        });\n        const thread = this.store.Thread.insert({ model: props.resModel, id: props.resId });\n        Object.assign(thread, {\n            access_token: chatterEl.getAttribute(\"data-token\"),\n            hash: chatterEl.getAttribute(\"data-hash\"),\n            pid: parseInt(chatterEl.getAttribute(\"data-pid\")),\n        });\n        const data = await rpc(\n            \"/portal/chatter_init\",\n            {\n                thread_model: props.resModel,\n                thread_id: props.resId,\n                ...thread.rpcParams,\n            },\n            { silent: true }\n        );\n        this.store.insert(data);\n        odoo.portalChatterReady.resolve(true);\n    }\n}\n\nexport const portalChatterService = {\n    dependencies: [\"mail.store\", \"bus_service\"],\n    start(env, services) {\n        const portalChatter = new PortalChatterService(env, services);\n        portalChatter.initialize(env);\n        return portalChatter;\n    },\n};\nregistry.category(\"services\").add(\"portal.chatter\", portalChatterService);\n", "import { Store } from \"@mail/core/common/store_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Store.prototype, {\n    async initialize() {\n        if (!this.initMessagingParams.init_messaging.channel_types) {\n            this.isReady.resolve();\n            return;\n        }\n        return super.initialize();\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    get rpcParams() {\n        return {\n            ...super.rpcParams,\n            ...(this.access_token ? { token: this.access_token } : {}),\n            ...(this.hash ? { hash: this.hash } : {}),\n            ...(this.pid ? { pid: this.pid } : {}),\n        };\n    },\n});\n", "import { Message } from \"@mail/core/common/message_model\";\nimport { Record } 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 = Record.one(\"rating.rating\");\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 { Composer } from \"@mail/core/common/composer\";\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useState } from \"@odoo/owl\";\n\npatch(Composer.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.portalState = useState({\n            ratingValue: 4,\n            starValue: 4,\n        });\n    },\n\n    get allowUpload() {\n        return super.allowUpload && !this.props.composer.portalComment;\n    },\n\n    editMessage() {\n        if (this.props.composer.portalComment) {\n            this.savePublisherComment();\n            return;\n        }\n        super.editMessage();\n    },\n\n    async savePublisherComment() {\n        const data = await rpc(\"/website/rating/comment\", {\n            rating_id: this.message.rating.id,\n            publisher_comment: this.props.composer.text.trim(),\n        });\n        this.message.rating = data;\n        this.props.onPostCallback();\n    },\n\n    onMoveStar(ev) {\n        const index = parseInt(ev.currentTarget.getAttribute(\"index\"));\n        this.portalState.starValue = index + 1;\n    },\n\n    onClickStar() {\n        this.portalState.ratingValue = this.portalState.starValue;\n    },\n\n    get postData() {\n        const postData = super.postData;\n        if (this.env.displayRating && !this.message) {\n            postData.rating_value = this.portalState.ratingValue;\n        }\n        return postData;\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\nimport { convertBrToLineBreak } from \"@mail/utils/common/format\";\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.state.editRating = false;\n    },\n\n    get ratingValue() {\n        return this.message.rating_id?.rating || this.message.rating_value;\n    },\n\n    onClikEditComment() {\n        this.state.editRating = !this.state.editRating;\n        if (this.state.editRating) {\n            const messageContent = convertBrToLineBreak(\n                this.props.message.rating.publisher_comment\n            );\n            this.props.message.composer = {\n                message: this.props.message,\n                text: messageContent,\n                portalComment: true,\n                selection: {\n                    start: messageContent.length,\n                    end: messageContent.length,\n                    direction: \"none\",\n                },\n            };\n        }\n    },\n\n    exitEditCommentMode() {\n        this.message.composer = null;\n        this.state.editRating = false;\n    },\n\n    async deleteComment() {\n        const data = await rpc(\"/website/rating/comment\", {\n            rating_id: this.message.rating.id,\n            publisher_comment: \"\",\n        });\n        this.message.rating = data;\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.rating_value) {\n            params.post_data.rating_value = postData.rating_value;\n        }\n        return params;\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    getFetchParams() {\n        const params = super.getFetchParams(...arguments);\n        if (this.model !== \"discuss.channel\") {\n            params[\"rating_include\"] = true;\n        }\n        return params;\n    },\n});\n"], "file": "/web/assets/1/e58008a/portal.assets_chatter.js", "sourceRoot": "../../../../"}