collavre 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +0 -1
- data/app/assets/stylesheets/collavre/comments_popup.css +165 -99
- data/app/assets/stylesheets/collavre/creatives.css +346 -37
- data/app/assets/stylesheets/collavre/desktop_setup.css +246 -0
- data/app/assets/stylesheets/collavre/gnb.css +64 -3
- data/app/assets/stylesheets/collavre/landing.css +108 -3
- data/app/assets/stylesheets/collavre/mention_menu.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +14 -0
- data/app/assets/stylesheets/collavre/print.css +0 -1
- data/app/assets/stylesheets/collavre/search_popup.css +1 -1
- data/app/assets/stylesheets/collavre/secret_fields.css +3 -0
- data/app/assets/stylesheets/collavre/workspace.css +307 -0
- data/app/channels/collavre/agent_channel.rb +14 -21
- data/app/channels/collavre/comments_presence_channel.rb +181 -21
- data/app/channels/collavre/inbox_badge_channel.rb +7 -1
- data/app/components/collavre/inbox/badge_component.rb +6 -13
- data/app/components/collavre/progress_filter_component.html.erb +2 -2
- data/app/components/collavre/progress_filter_component.rb +20 -0
- data/app/controllers/collavre/admin/settings_controller.rb +4 -1
- data/app/controllers/collavre/agent_connections_controller.rb +165 -0
- data/app/controllers/collavre/agent_gateways_controller.rb +132 -0
- data/app/controllers/collavre/agent_provisioning_controller.rb +79 -0
- data/app/controllers/collavre/api/v1/agents_controller.rb +45 -97
- data/app/controllers/collavre/application_controller.rb +13 -2
- data/app/controllers/collavre/comment_read_pointers_controller.rb +69 -40
- data/app/controllers/collavre/comments_controller.rb +45 -131
- data/app/controllers/collavre/concerns/shareable.rb +9 -3
- data/app/controllers/collavre/creatives_controller.rb +144 -11
- data/app/controllers/collavre/desktop_gateway_registrations_controller.rb +173 -0
- data/app/controllers/collavre/desktop_setup_controller.rb +119 -0
- data/app/controllers/collavre/desktop_sidecar_health_controller.rb +35 -0
- data/app/controllers/collavre/features_controller.rb +57 -0
- data/app/controllers/collavre/invites_controller.rb +12 -1
- data/app/controllers/collavre/llm_models_controller.rb +8 -0
- data/app/controllers/collavre/notices_controller.rb +20 -0
- data/app/controllers/collavre/tasks_controller.rb +40 -6
- data/app/controllers/collavre/topics_controller.rb +130 -53
- data/app/controllers/collavre/user_creative_preferences_controller.rb +214 -30
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +15 -3
- data/app/controllers/concerns/collavre/comments/conversion.rb +1 -1
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +118 -8
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +1 -15
- data/app/controllers/concerns/collavre/users_controller/registration.rb +2 -2
- data/app/errors/collavre/turn_deadline_error.rb +20 -0
- data/app/helpers/collavre/application_helper.rb +38 -0
- data/app/helpers/collavre/creatives_helper.rb +56 -22
- data/app/helpers/collavre/navigation_helper.rb +12 -0
- data/app/javascript/collavre.js +1 -2
- data/app/javascript/components/InlineLexicalEditor.jsx +30 -21
- data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +32 -2
- data/app/javascript/components/__tests__/creative_tree_row_progress_toggle.test.js +185 -0
- data/app/javascript/components/creative_tree_row.js +62 -22
- data/app/javascript/controllers/__tests__/agent_connection_controller.test.js +195 -0
- data/app/javascript/controllers/__tests__/desktop_proxy_setup_controller.test.js +96 -0
- data/app/javascript/controllers/__tests__/index.test.js +42 -0
- data/app/javascript/controllers/__tests__/last_visited_creative_controller.test.js +103 -0
- data/app/javascript/controllers/__tests__/link_creative_controller.test.js +253 -2
- data/app/javascript/controllers/__tests__/progress_filter_controller.test.js +134 -0
- data/app/javascript/controllers/__tests__/search_popup_controller.test.js +310 -0
- data/app/javascript/controllers/__tests__/topic_list_controller.test.js +111 -0
- data/app/javascript/controllers/__tests__/workspace_tree_controller.test.js +1011 -0
- data/app/javascript/controllers/agent_connection_controller.js +357 -0
- data/app/javascript/controllers/agent_vendor_controller.js +23 -0
- data/app/javascript/controllers/comment_controller.js +3 -1
- data/app/javascript/controllers/comments/__tests__/feature_cards_controller.test.js +223 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_default_placeholder.test.js +83 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_draft.test.js +2069 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_ime_send.test.js +324 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +28 -2
- data/app/javascript/controllers/comments/__tests__/form_controller_search_submit.test.js +106 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft.test.js +134 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_creative_switch.test.js +166 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_failure.test.js +183 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_deep_link_topic.test.js +467 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_docked.test.js +115 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_prev_message.test.js +245 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_read_pointer.test.js +276 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_archived.test.js +221 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_search.test.js +104 -0
- data/app/javascript/controllers/comments/__tests__/placeholder_controller.test.js +79 -0
- data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +754 -1
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +717 -15
- data/app/javascript/controllers/comments/__tests__/prev_message_navigator.test.js +129 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_action_errors.test.js +184 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_archived.test.js +879 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_container.test.js +154 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_scroll.test.js +92 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +130 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_docked.test.js +32 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_last_topic_echo.test.js +1938 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_primary_agent.test.js +256 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_scroll.test.js +182 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_selection_race.test.js +981 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +313 -0
- data/app/javascript/controllers/comments/feature_cards_controller.js +83 -0
- data/app/javascript/controllers/comments/form_controller.js +795 -14
- data/app/javascript/controllers/comments/list_controller.js +338 -52
- data/app/javascript/controllers/comments/placeholder_controller.js +30 -0
- data/app/javascript/controllers/comments/popup_controller.js +425 -83
- data/app/javascript/controllers/comments/presence_controller.js +317 -55
- data/app/javascript/controllers/comments/prev_message_navigator.js +94 -0
- data/app/javascript/controllers/comments/topics_controller.js +1772 -100
- data/app/javascript/controllers/common_popup_controller.js +2 -2
- data/app/javascript/controllers/creatives/__tests__/select_mode_controller.test.js +139 -0
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +605 -0
- data/app/javascript/controllers/creatives/__tests__/write_access_request_controller.test.js +125 -0
- data/app/javascript/controllers/creatives/select_mode_controller.js +7 -2
- data/app/javascript/controllers/creatives/tree_controller.js +140 -31
- data/app/javascript/controllers/creatives/write_access_request_controller.js +58 -0
- data/app/javascript/controllers/desktop_proxy_setup_controller.js +51 -0
- data/app/javascript/controllers/gateway_check_controller.js +36 -0
- data/app/javascript/controllers/index.js +25 -1
- data/app/javascript/controllers/last_visited_creative_controller.js +58 -0
- data/app/javascript/controllers/link_creative_controller.js +89 -18
- data/app/javascript/controllers/progress_filter_controller.js +12 -22
- data/app/javascript/controllers/search_popup_controller.js +64 -46
- data/app/javascript/controllers/share_user_search_controller.js +2 -2
- data/app/javascript/controllers/topic_list_controller.js +44 -7
- data/app/javascript/controllers/workspace_tree_controller.js +662 -0
- data/app/javascript/creatives/__tests__/tree_renderer.test.js +80 -0
- data/app/javascript/creatives/drag_drop/__tests__/drop_completion_empty_state.test.js +98 -0
- data/app/javascript/creatives/drag_drop/__tests__/topic_move_released_agent.test.js +125 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +12 -0
- data/app/javascript/creatives/tree_renderer.js +65 -22
- data/app/javascript/lib/__tests__/chat_drafts.test.js +1458 -0
- data/app/javascript/lib/__tests__/common_popup_bounds.test.js +8 -3
- data/app/javascript/lib/__tests__/common_popup_placement.test.js +374 -0
- data/app/javascript/lib/__tests__/common_popup_reshow.test.js +131 -0
- data/app/javascript/lib/__tests__/creative_link_prefetch.test.js +46 -0
- data/app/javascript/lib/__tests__/last_visited_creative.test.js +87 -0
- data/app/javascript/lib/__tests__/skill_html_entities.test.js +23 -0
- data/app/javascript/lib/__tests__/turbo_stream_actions.test.js +190 -0
- data/app/javascript/lib/api/__tests__/creatives.test.js +68 -0
- data/app/javascript/lib/api/__tests__/csrf_fetch.test.js +27 -0
- data/app/javascript/lib/api/__tests__/topics_save_last_topic.test.js +112 -0
- data/app/javascript/lib/api/creatives.js +32 -13
- data/app/javascript/lib/api/csrf_fetch.js +6 -4
- data/app/javascript/lib/api/topics.js +63 -5
- data/app/javascript/lib/chat_drafts.js +786 -0
- data/app/javascript/lib/common_popup.js +164 -25
- data/app/javascript/lib/creative_link_prefetch.js +27 -0
- data/app/javascript/lib/last_visited_creative.js +97 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_node.test.js +162 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_trigger.test.js +133 -0
- data/app/javascript/lib/lexical/__tests__/link_toolbar.test.js +62 -0
- data/app/javascript/lib/lexical/creative_link_node.js +101 -0
- data/app/javascript/lib/lexical/creative_link_trigger.js +98 -0
- data/app/javascript/lib/lexical/link_toolbar.js +23 -0
- data/app/javascript/lib/turbo_stream_actions.js +23 -16
- data/app/javascript/lib/utils/__tests__/filter_navigation.test.js +210 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +44 -0
- data/app/javascript/lib/utils/filter_navigation.js +100 -0
- data/app/javascript/modules/__tests__/command_menu_creative_picker.test.js +62 -0
- data/app/javascript/modules/__tests__/command_menu_typeahead.test.js +267 -0
- data/app/javascript/modules/__tests__/creative_link_picker.test.js +85 -0
- data/app/javascript/modules/__tests__/creative_row_editor_archive.test.js +391 -0
- data/app/javascript/modules/__tests__/creative_row_editor_delegated_clicks.test.js +158 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_recovery.test.js +519 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_removal.test.js +346 -0
- data/app/javascript/modules/__tests__/creative_row_editor_session.test.js +144 -0
- data/app/javascript/modules/__tests__/creative_tree_dom.test.js +30 -0
- data/app/javascript/modules/__tests__/creative_tree_empty_state.test.js +186 -0
- data/app/javascript/modules/__tests__/dom_listener_registry.test.js +67 -0
- data/app/javascript/modules/__tests__/support/inline_editor_dom.js +159 -0
- data/app/javascript/modules/command_args_form.js +0 -12
- data/app/javascript/modules/command_menu.js +80 -38
- data/app/javascript/modules/creative_link_picker.js +56 -0
- data/app/javascript/modules/creative_row_editor.js +446 -243
- data/app/javascript/modules/creative_row_editor_delegated_clicks.js +87 -0
- data/app/javascript/modules/creative_tree_dom.js +4 -0
- data/app/javascript/modules/creative_tree_empty_state.js +80 -0
- data/app/javascript/modules/dom_listener_registry.js +23 -0
- data/app/javascript/modules/mention_menu.js +2 -3
- data/app/javascript/utils/__tests__/caret_anchor.test.js +75 -0
- data/app/javascript/utils/__tests__/chevron_icons.test.js +15 -0
- data/app/javascript/utils/caret_position.js +10 -0
- data/app/javascript/utils/chevron_icons.js +7 -0
- data/app/jobs/collavre/ai_agent_job.rb +262 -72
- data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +11 -36
- data/app/jobs/collavre/comment_badges_broadcast_job.rb +28 -0
- data/app/jobs/collavre/comment_link_preview_job.rb +33 -0
- data/app/jobs/collavre/comment_notification_job.rb +14 -0
- data/app/jobs/collavre/comment_push_delivery_sweep_job.rb +17 -0
- data/app/jobs/collavre/compress_job.rb +33 -22
- data/app/jobs/collavre/creative_broadcast_job.rb +161 -48
- data/app/jobs/collavre/cron_action_job.rb +36 -18
- data/app/jobs/collavre/drop_trigger_job.rb +23 -9
- data/app/jobs/collavre/merge_comments_job.rb +33 -16
- data/app/jobs/collavre/push_notification_job.rb +62 -18
- data/app/jobs/collavre/restore_dropped_dispatches_job.rb +17 -0
- data/app/jobs/collavre/stuck_detector_job.rb +1 -1
- data/app/jobs/collavre/touch_creative_subtree_job.rb +14 -0
- data/app/models/collavre/agent_gateway.rb +200 -0
- data/app/models/collavre/agent_subscription.rb +4 -4
- data/app/models/collavre/agent_workspace.rb +210 -0
- data/app/models/collavre/comment/broadcastable.rb +22 -52
- data/app/models/collavre/comment/notifiable.rb +154 -77
- data/app/models/collavre/comment/topic_membership.rb +41 -0
- data/app/models/collavre/comment.rb +336 -31
- data/app/models/collavre/comment_notification_delivery.rb +61 -0
- data/app/models/collavre/comment_presence_store/cache_batches.rb +64 -0
- data/app/models/collavre/comment_presence_store.rb +272 -12
- data/app/models/collavre/comment_read_pointer.rb +9 -15
- data/app/models/collavre/creative/describable.rb +8 -2
- data/app/models/collavre/creative/permissible.rb +11 -0
- data/app/models/collavre/creative.rb +29 -4
- data/app/models/collavre/creative_share.rb +109 -4
- data/app/models/collavre/current.rb +1 -0
- data/app/models/collavre/llm_model.rb +62 -0
- data/app/models/collavre/system_setting.rb +12 -1
- data/app/models/collavre/task.rb +121 -2
- data/app/models/collavre/topic.rb +51 -0
- data/app/models/collavre/user.rb +101 -41
- data/app/models/collavre/user_creative_preference.rb +3 -1
- data/app/services/collavre/agent_provisioning/archive.rb +108 -0
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +16 -3
- data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +63 -6
- data/app/services/collavre/ai_agent/approval_handler.rb +12 -4
- data/app/services/collavre/ai_agent/claude_channel_adapter.rb +10 -5
- data/app/services/collavre/ai_agent/merged_trigger_comments.rb +164 -0
- data/app/services/collavre/ai_agent/message_builder.rb +103 -16
- data/app/services/collavre/ai_agent/session_provisioner.rb +3 -7
- data/app/services/collavre/ai_agent/task_claim_service.rb +29 -15
- data/app/services/collavre/ai_agent/task_reply_service.rb +67 -0
- data/app/services/collavre/ai_agent_service.rb +108 -20
- data/app/services/collavre/ai_client.rb +211 -21
- data/app/services/collavre/cli_proxy/client.rb +147 -0
- data/app/services/collavre/cli_proxy/endpoint_policy.rb +81 -0
- data/app/services/collavre/cli_proxy/identity.rb +47 -0
- data/app/services/collavre/cli_proxy/safe_net_http_adapter.rb +19 -0
- data/app/services/collavre/command_menu_service.rb +0 -10
- data/app/services/collavre/comment_move_service.rb +96 -7
- data/app/services/collavre/comment_snapshot_restore_service.rb +11 -5
- data/app/services/collavre/comments/action_executor.rb +48 -20
- data/app/services/collavre/comments/command_processor.rb +0 -1
- data/app/services/collavre/comments/list_response.rb +133 -0
- data/app/services/collavre/comments/read_pointer_writer.rb +210 -0
- data/app/services/collavre/comments/read_receipt_broadcaster.rb +157 -0
- data/app/services/collavre/comments/read_receipt_index.rb +152 -0
- data/app/services/collavre/comments/topic_command.rb +72 -1
- data/app/services/collavre/comments/topic_mutation.rb +20 -0
- data/app/services/collavre/creatives/breadcrumb_resolver.rb +1 -1
- data/app/services/collavre/creatives/comment_badge_index.rb +299 -27
- data/app/services/collavre/creatives/reorderer.rb +15 -3
- data/app/services/collavre/creatives/tree_builder.rb +1 -1
- data/app/services/collavre/creatives/tree_formatter.rb +5 -6
- data/app/services/collavre/creatives/workspace_path_resolver.rb +50 -0
- data/app/services/collavre/creatives/workspace_tree_builder.rb +92 -0
- data/app/services/collavre/crons/recurring_topic_tasks.rb +23 -0
- data/app/services/collavre/home_page_path.rb +55 -0
- data/app/services/collavre/html_text.rb +64 -0
- data/app/services/collavre/http_client.rb +11 -2
- data/app/services/collavre/inbox_reply_service.rb +1 -1
- data/app/services/collavre/markdown_converter.rb +13 -1
- data/app/services/collavre/orchestration/agent_orchestrator.rb +776 -81
- data/app/services/collavre/orchestration/arbiter.rb +48 -5
- data/app/services/collavre/orchestration/deferred_trigger_scope.rb +53 -0
- data/app/services/collavre/orchestration/delivery_record.rb +792 -0
- data/app/services/collavre/orchestration/loop_breaker.rb +10 -4
- data/app/services/collavre/orchestration/matcher.rb +132 -17
- data/app/services/collavre/orchestration/policy_resolver.rb +58 -10
- data/app/services/collavre/orchestration/scheduler.rb +10 -4
- data/app/services/collavre/orchestration/scheduling_hooks.rb +10 -0
- data/app/services/collavre/orchestration/selection.rb +43 -0
- data/app/services/collavre/orchestration/stuck_detector.rb +25 -112
- data/app/services/collavre/orchestration/task_coalescer.rb +355 -0
- data/app/services/collavre/orchestration/topic_slot.rb +73 -0
- data/app/services/collavre/system_events/context_builder.rb +60 -11
- data/app/services/collavre/system_events/dispatcher.rb +58 -12
- data/app/services/collavre/system_events/envelope.rb +73 -0
- data/app/services/collavre/system_events/vocabulary.rb +45 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +1 -1
- data/app/services/collavre/tools/cron_create_service.rb +32 -18
- data/app/services/collavre/tools/id_list.rb +38 -0
- data/app/services/collavre/tools/topic_authorizer.rb +61 -7
- data/app/services/collavre/tools/topic_branch_service.rb +85 -0
- data/app/services/collavre/tools/topic_create_service.rb +117 -0
- data/app/services/collavre/tools/topic_list_service.rb +101 -0
- data/app/services/collavre/tools/topic_message_create_service.rb +192 -0
- data/app/services/collavre/tools/topic_messages_service.rb +346 -0
- data/app/services/collavre/tools/topic_move_service.rb +108 -0
- data/app/services/collavre/tools/topic_selection.rb +59 -0
- data/app/services/collavre/tools/topic_update_service.rb +165 -0
- data/app/services/collavre/topic_branch_service.rb +39 -10
- data/app/services/collavre/topics/agent_resolver.rb +87 -0
- data/app/services/collavre/topics/char_budget.rb +70 -0
- data/app/services/collavre/topics/last_topic_preference_payload.rb +18 -0
- data/app/services/collavre/topics/message_page.rb +380 -0
- data/app/services/collavre/topics/message_page_markdown.rb +102 -0
- data/app/services/collavre/topics/message_scope.rb +61 -0
- data/app/services/collavre/topics/message_stats.rb +92 -0
- data/app/services/collavre/topics/move_blocker.rb +47 -0
- data/app/services/collavre/topics/next_name.rb +35 -0
- data/app/services/collavre/topics/pin_rejection.rb +25 -0
- data/app/services/collavre/topics/read_pointer_relocator.rb +71 -0
- data/app/services/collavre/topics/reserved_name.rb +18 -0
- data/app/services/collavre/topics/serializer.rb +92 -0
- data/app/services/collavre/topics/topic_destroy.rb +30 -0
- data/app/services/collavre/topics/topic_move.rb +80 -0
- data/app/services/collavre/topics/topic_move_effects.rb +35 -0
- data/app/services/collavre/topics/topic_mutation.rb +27 -0
- data/app/views/collavre/admin/integrations/_setting_row.html.erb +5 -8
- data/app/views/collavre/admin/settings/_system_tab.html.erb +7 -0
- data/app/views/collavre/agent_connections/show.html.erb +77 -0
- data/app/views/collavre/agent_gateways/_form.html.erb +71 -0
- data/app/views/collavre/agent_gateways/edit.html.erb +2 -0
- data/app/views/collavre/agent_gateways/index.html.erb +54 -0
- data/app/views/collavre/agent_gateways/new.html.erb +2 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +32 -17
- data/app/views/collavre/comments/_empty_state.html.erb +33 -0
- data/app/views/collavre/comments/_empty_state_minimal.html.erb +7 -0
- data/app/views/collavre/comments/_feature_card.html.erb +35 -0
- data/app/views/collavre/comments/_list.html.erb +3 -2
- data/app/views/collavre/comments/_no_search_results.html.erb +3 -0
- data/app/views/collavre/creatives/_add_button.html.erb +17 -19
- data/app/views/collavre/creatives/_empty_state.html.erb +109 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +5 -5
- data/app/views/collavre/creatives/_tree_loading_placeholder.html.erb +16 -0
- data/app/views/collavre/creatives/_workspace_tree.html.erb +33 -0
- data/app/views/collavre/creatives/index.html.erb +79 -18
- data/app/views/collavre/desktop_setup/show.html.erb +114 -0
- data/app/views/collavre/features/index.html.erb +42 -0
- data/app/views/collavre/features/show.html.erb +52 -0
- data/app/views/collavre/invitation_mailer/invite.html.erb +1 -1
- data/app/views/collavre/invitation_mailer/invite.text.erb +1 -1
- data/app/views/collavre/invites/show.html.erb +14 -1
- data/app/views/collavre/landing/show.html.erb +5 -1
- data/app/views/collavre/shared/_link_creative_modal.html.erb +4 -1
- data/app/views/collavre/shared/navigation/_help_button.html.erb +3 -1
- data/app/views/collavre/shared/navigation/_panels.html.erb +1 -8
- data/app/views/collavre/shared/navigation/_search_form.html.erb +13 -2
- data/app/views/collavre/user_themes/index.html.erb +1 -1
- data/app/views/collavre/users/_llm_fields.html.erb +65 -0
- data/app/views/collavre/users/_org_chart_node.html.erb +1 -1
- data/app/views/collavre/users/edit_ai.html.erb +33 -31
- data/app/views/collavre/users/new.html.erb +4 -2
- data/app/views/collavre/users/new_ai.html.erb +14 -32
- data/app/views/collavre/users/show.html.erb +13 -1
- data/app/views/layouts/collavre/landing.html.erb +9 -4
- data/config/initializers/ruby_llm_openai_reasoning_content.rb +77 -0
- data/config/locales/admin.en.yml +3 -1
- data/config/locales/admin.ko.yml +3 -1
- data/config/locales/agent_gateway.en.yml +217 -0
- data/config/locales/agent_gateway.ko.yml +217 -0
- data/config/locales/ai_agent.en.yml +0 -1
- data/config/locales/ai_agent.ko.yml +0 -1
- data/config/locales/comments.en.yml +72 -30
- data/config/locales/comments.ko.yml +72 -30
- data/config/locales/creatives.en.yml +32 -1
- data/config/locales/creatives.ko.yml +32 -1
- data/config/locales/features.en.yml +140 -0
- data/config/locales/features.ko.yml +140 -0
- data/config/locales/invites.en.yml +3 -0
- data/config/locales/invites.ko.yml +3 -0
- data/config/locales/landing.en.yml +2 -1
- data/config/locales/landing.ko.yml +2 -1
- data/config/locales/tools.en.yml +20 -0
- data/config/locales/tools.ko.yml +20 -0
- data/config/locales/users.en.yml +15 -14
- data/config/locales/users.ko.yml +14 -14
- data/config/routes.rb +45 -19
- data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +9 -1
- data/db/migrate/20260724000000_add_creative_id_index_to_comments.rb +34 -0
- data/db/migrate/20260724000001_add_creative_private_id_index_to_comments.rb +50 -0
- data/db/migrate/20260724090000_add_async_comment_notification_state.rb +64 -0
- data/db/migrate/20260727000000_add_waiting_notice_scope_to_comments.rb +30 -0
- data/db/migrate/20260727010000_add_waiting_notice_scope_to_tasks.rb +26 -0
- data/db/migrate/20260805000000_add_creative_workspace_enabled_to_users.rb +5 -0
- data/db/migrate/20260807050000_add_dismissed_notices_to_users.rb +5 -0
- data/db/migrate/20260809000000_change_creative_workspace_enabled_default_to_true.rb +18 -0
- data/db/migrate/20260809000001_create_llm_models.rb +64 -0
- data/db/migrate/20260809000002_create_agent_gateways_and_workspaces.rb +35 -0
- data/db/migrate/20260809000003_hash_agent_workspace_callback_tokens.rb +44 -0
- data/db/migrate/20260809000004_encrypt_agent_workspace_manifest_tokens.rb +59 -0
- data/db/migrate/20260810000000_split_agent_workspace_identity_axes.rb +58 -0
- data/db/migrate/20260810000001_seed_codex_custom_proxy_llm_models.rb +28 -0
- data/db/migrate/20260810010000_add_last_visited_creative_to_users.rb +8 -0
- data/db/migrate/20260810020000_add_last_visited_creative_at_to_users.rb +5 -0
- data/db/migrate/20260810030000_add_last_visited_creative_navigation_order_to_users.rb +6 -0
- data/db/migrate/20260810060000_add_last_visited_creative_issued_sequence_to_users.rb +5 -0
- data/db/migrate/20260810100000_add_desktop_managed_to_agent_gateways.rb +40 -0
- data/db/migrate/20260810100001_add_desktop_preset_adapter_to_users.rb +38 -0
- data/db/migrate/20260811000000_make_agent_gateway_completion_key_nullable.rb +7 -0
- data/db/migrate/20260812000000_remove_workflow_columns_from_tasks.rb +101 -0
- data/db/migrate/20260812000001_remove_android_devices.rb +24 -0
- data/db/migrate/20260812010000_drop_inbox_items.rb +22 -0
- data/db/migrate/20260813000000_clear_claude_channel_presence_routing_expressions.rb +16 -0
- data/db/migrate/20260813000001_add_last_topic_revision_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000002_add_last_topic_save_order_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260813000003_add_last_topic_save_sequences_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000004_add_last_topic_save_fences_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260814000000_add_topic_to_comment_read_pointers.rb +79 -0
- data/db/migrate/20260814000001_add_creative_topic_private_id_index_to_comments.rb +37 -0
- data/db/migrate/20260816000000_add_watermark_index_to_comment_read_pointers.rb +27 -0
- data/db/migrate/20260816000001_add_private_recipient_topic_index_to_comments.rb +48 -0
- data/db/migrate/20260820000000_add_topic_assigned_at_to_comments.rb +13 -0
- data/db/seeds.rb +1 -51
- data/lib/collavre/engine.rb +108 -1
- data/lib/collavre/feature_card_registry.rb +168 -0
- data/lib/collavre/hashed_access_token_lookup.rb +22 -0
- data/lib/collavre/sensitive_request_silencer.rb +23 -0
- data/lib/collavre/user_extensions.rb +0 -2
- data/lib/collavre/version.rb +1 -1
- data/lib/collavre.rb +1 -0
- data/skills/collavre/SKILL.md +165 -0
- data/skills/collavre/package.json +8 -0
- data/skills/collavre/references/tool-reference.md +347 -0
- data/skills/collavre/scripts/collavre +634 -0
- data/skills/collavre/scripts/html_entities.js +24 -0
- metadata +228 -30
- data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +0 -224
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +0 -95
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +0 -31
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +0 -25
- data/app/controllers/collavre/inbox_items_controller.rb +0 -70
- data/app/controllers/collavre/typo_corrections_controller.rb +0 -39
- data/app/javascript/lib/__tests__/typo_correction.test.js +0 -192
- data/app/javascript/lib/typo_correction.js +0 -146
- data/app/javascript/modules/__tests__/typo_corrector.test.js +0 -365
- data/app/javascript/modules/creative_guide.js +0 -67
- data/app/javascript/modules/typo_corrector.js +0 -534
- data/app/jobs/collavre/inbox_summary_job.rb +0 -24
- data/app/mailers/collavre/inbox_mailer.rb +0 -19
- data/app/models/collavre/inbox_item.rb +0 -89
- data/app/services/collavre/comments/concerns/workflow_support.rb +0 -115
- data/app/services/collavre/comments/work_command.rb +0 -161
- data/app/services/collavre/comments/workflow_executor.rb +0 -276
- data/app/services/collavre/gemini_parent_recommender.rb +0 -77
- data/app/services/collavre/mobile/event_summarizer.rb +0 -40
- data/app/services/collavre/typo_corrector.rb +0 -188
- data/app/views/collavre/inbox_items/_item.html.erb +0 -18
- data/app/views/collavre/inbox_items/_items.html.erb +0 -3
- data/app/views/collavre/inbox_items/_list.html.erb +0 -7
- data/app/views/collavre/inbox_items/index.html.erb +0 -1
- data/app/views/collavre/inbox_mailer/daily_summary.html.erb +0 -11
- data/app/views/collavre/inbox_mailer/daily_summary.text.erb +0 -8
- data/app/views/collavre/users/typo_correction.html.erb +0 -50
- data/config/locales/mobile.en.yml +0 -16
- data/config/locales/mobile.ko.yml +0 -16
|
@@ -18,6 +18,7 @@ jest.unstable_mockModule('../../../utils/emoji_parser', () => ({
|
|
|
18
18
|
const { Application } = await import('@hotwired/stimulus')
|
|
19
19
|
const TreeController = (await import('../tree_controller')).default
|
|
20
20
|
const { appendCreativeNodes } = await import('../../../creatives/tree_renderer')
|
|
21
|
+
const { restoreTreeEmptyState } = await import('../../../modules/creative_tree_empty_state')
|
|
21
22
|
|
|
22
23
|
const TRANSIENT_RETRY_DELAYS = [200, 600]
|
|
23
24
|
|
|
@@ -32,6 +33,7 @@ const installController = () => {
|
|
|
32
33
|
const container = document.createElement('div')
|
|
33
34
|
container.setAttribute('data-controller', 'creatives--tree')
|
|
34
35
|
container.setAttribute('data-creatives--tree-url-value', '/creatives?format=json&id=991')
|
|
36
|
+
container.setAttribute('data-creatives--tree-loading-text-value', 'Loading creatives')
|
|
35
37
|
document.body.appendChild(container)
|
|
36
38
|
|
|
37
39
|
const application = Application.start()
|
|
@@ -267,4 +269,607 @@ describe('CreativesTreeController Chats pagination (load more)', () => {
|
|
|
267
269
|
|
|
268
270
|
application.stop()
|
|
269
271
|
})
|
|
272
|
+
|
|
273
|
+
// Deleting every row that is currently on screen does not mean the feed is
|
|
274
|
+
// empty when further pages are still queued behind the sentinel. Restoring the
|
|
275
|
+
// placeholder there both lies and outlives the append, leaving "No creatives
|
|
276
|
+
// found." sitting above the rows the next page brings in.
|
|
277
|
+
describe('empty-state placeholder vs. queued pages', () => {
|
|
278
|
+
const installEmptyStateTemplate = () => {
|
|
279
|
+
const template = document.createElement('template')
|
|
280
|
+
template.id = 'creatives-empty-state-template'
|
|
281
|
+
template.innerHTML = '<p data-creatives-empty-state>No creatives found.</p>'
|
|
282
|
+
document.body.appendChild(template)
|
|
283
|
+
return template
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
test('suppresses the placeholder while the feed has more pages queued', async () => {
|
|
287
|
+
installEmptyStateTemplate()
|
|
288
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
289
|
+
ok: true,
|
|
290
|
+
json: async () => ({ creatives: [{ id: 1 }], pagination: { has_more: true, next_page: 2 } }),
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const { container, application } = installController()
|
|
294
|
+
await flush()
|
|
295
|
+
await flush()
|
|
296
|
+
|
|
297
|
+
expect(container.hasAttribute('data-creatives-pagination-pending')).toBe(true)
|
|
298
|
+
|
|
299
|
+
// The user batch-deletes every rendered row; removeTreeElement asks for the
|
|
300
|
+
// placeholder back.
|
|
301
|
+
restoreTreeEmptyState(container)
|
|
302
|
+
|
|
303
|
+
expect(container.querySelector('[data-creatives-empty-state]')).toBeNull()
|
|
304
|
+
|
|
305
|
+
application.stop()
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('restores the placeholder once the last page lands and nothing is left', async () => {
|
|
309
|
+
installEmptyStateTemplate()
|
|
310
|
+
global.fetch = jest
|
|
311
|
+
.fn()
|
|
312
|
+
.mockResolvedValueOnce({
|
|
313
|
+
ok: true,
|
|
314
|
+
json: async () => ({ creatives: [{ id: 1 }], pagination: { has_more: true, next_page: 2 } }),
|
|
315
|
+
})
|
|
316
|
+
.mockResolvedValueOnce({
|
|
317
|
+
ok: true,
|
|
318
|
+
json: async () => ({ creatives: [], pagination: { has_more: false, next_page: null } }),
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
const { container, application } = installController()
|
|
322
|
+
await flush()
|
|
323
|
+
await flush()
|
|
324
|
+
|
|
325
|
+
restoreTreeEmptyState(container)
|
|
326
|
+
expect(container.querySelector('[data-creatives-empty-state]')).toBeNull()
|
|
327
|
+
|
|
328
|
+
MockIntersectionObserver.instances[0].triggerIntersect()
|
|
329
|
+
await flush()
|
|
330
|
+
await flush()
|
|
331
|
+
|
|
332
|
+
expect(container.hasAttribute('data-creatives-pagination-pending')).toBe(false)
|
|
333
|
+
const placeholder = container.querySelector('[data-creatives-empty-state]')
|
|
334
|
+
expect(placeholder).not.toBeNull()
|
|
335
|
+
expect(placeholder.hidden).toBe(false)
|
|
336
|
+
|
|
337
|
+
application.stop()
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test('hides a placeholder that is on screen when the next page is appended', async () => {
|
|
341
|
+
installEmptyStateTemplate()
|
|
342
|
+
global.fetch = jest
|
|
343
|
+
.fn()
|
|
344
|
+
.mockResolvedValueOnce({
|
|
345
|
+
ok: true,
|
|
346
|
+
json: async () => ({ creatives: [{ id: 1 }], pagination: { has_more: true, next_page: 2 } }),
|
|
347
|
+
})
|
|
348
|
+
.mockResolvedValueOnce({
|
|
349
|
+
ok: true,
|
|
350
|
+
json: async () => ({ creatives: [{ id: 2 }], pagination: { has_more: true, next_page: 3 } }),
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const { container, application } = installController()
|
|
354
|
+
await flush()
|
|
355
|
+
await flush()
|
|
356
|
+
|
|
357
|
+
// Belt and braces: a placeholder that got in anyway (a path that predates
|
|
358
|
+
// the pending marker) must not survive the append.
|
|
359
|
+
const stray = document.createElement('p')
|
|
360
|
+
stray.setAttribute('data-creatives-empty-state', '')
|
|
361
|
+
container.appendChild(stray)
|
|
362
|
+
|
|
363
|
+
MockIntersectionObserver.instances[0].triggerIntersect()
|
|
364
|
+
await flush()
|
|
365
|
+
await flush()
|
|
366
|
+
|
|
367
|
+
expect(appendCreativeNodes).toHaveBeenCalledTimes(1)
|
|
368
|
+
expect(stray.hidden).toBe(true)
|
|
369
|
+
|
|
370
|
+
application.stop()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test('clears the pending marker when a fresh load tears pagination down', async () => {
|
|
374
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
375
|
+
ok: true,
|
|
376
|
+
json: async () => ({ creatives: [{ id: 1 }], pagination: { has_more: true, next_page: 2 } }),
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const { container, application } = installController()
|
|
380
|
+
await flush()
|
|
381
|
+
await flush()
|
|
382
|
+
expect(container.hasAttribute('data-creatives-pagination-pending')).toBe(true)
|
|
383
|
+
|
|
384
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
385
|
+
ok: true,
|
|
386
|
+
json: async () => ({ creatives: [{ id: 9 }] }),
|
|
387
|
+
})
|
|
388
|
+
application.getControllerForElementAndIdentifier(container, 'creatives--tree').load()
|
|
389
|
+
await flush()
|
|
390
|
+
await flush()
|
|
391
|
+
|
|
392
|
+
expect(container.hasAttribute('data-creatives-pagination-pending')).toBe(false)
|
|
393
|
+
|
|
394
|
+
application.stop()
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
describe('CreativesTreeController error state vs genuine-empty state', () => {
|
|
400
|
+
let originalFetch
|
|
401
|
+
|
|
402
|
+
const installControllerWithStates = () => {
|
|
403
|
+
// Mirrors index.html.erb: the actionable empty state lives in the server-rendered
|
|
404
|
+
// <template> that showEmptyState() clones, while the load-error fallback is a bare
|
|
405
|
+
// translated sentence the controller wraps in a <p> itself.
|
|
406
|
+
const template = document.createElement('template')
|
|
407
|
+
template.id = 'creatives-empty-state-template'
|
|
408
|
+
template.innerHTML =
|
|
409
|
+
'<div data-creatives-empty-state><div class="creative-empty-state">' +
|
|
410
|
+
'<button class="new-root-creative-btn">Add</button></div></div>'
|
|
411
|
+
document.body.appendChild(template)
|
|
412
|
+
|
|
413
|
+
const container = document.createElement('div')
|
|
414
|
+
container.setAttribute('data-controller', 'creatives--tree')
|
|
415
|
+
container.setAttribute('data-creatives--tree-url-value', '/creatives?format=json&id=991')
|
|
416
|
+
container.setAttribute('data-creatives--tree-loading-text-value', 'Loading creatives')
|
|
417
|
+
container.setAttribute(
|
|
418
|
+
'data-creatives--tree-error-text-value',
|
|
419
|
+
'Could not load the creative tree.'
|
|
420
|
+
)
|
|
421
|
+
document.body.appendChild(container)
|
|
422
|
+
|
|
423
|
+
const application = Application.start()
|
|
424
|
+
application.register('creatives--tree', TreeController)
|
|
425
|
+
|
|
426
|
+
return { container, application }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
beforeEach(() => {
|
|
430
|
+
originalFetch = global.fetch
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
afterEach(() => {
|
|
434
|
+
global.fetch = originalFetch
|
|
435
|
+
document.body.innerHTML = ''
|
|
436
|
+
jest.restoreAllMocks()
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
test('a non-2xx response renders the distinct error state, not the actionable empty-state CTAs', async () => {
|
|
440
|
+
jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
441
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
442
|
+
ok: false,
|
|
443
|
+
status: 500,
|
|
444
|
+
json: async () => ({}),
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
const { container, application } = installControllerWithStates()
|
|
448
|
+
await flush()
|
|
449
|
+
await flush()
|
|
450
|
+
await flush()
|
|
451
|
+
|
|
452
|
+
expect(container.querySelector('.creative-tree-error')).not.toBeNull()
|
|
453
|
+
expect(container.querySelector('.creative-empty-state')).toBeNull()
|
|
454
|
+
expect(container.querySelector('.new-root-creative-btn')).toBeNull()
|
|
455
|
+
|
|
456
|
+
application.stop()
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
test('a JSON parse failure renders the distinct error state, not the actionable empty-state CTAs', async () => {
|
|
460
|
+
jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
461
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
462
|
+
ok: true,
|
|
463
|
+
json: async () => {
|
|
464
|
+
throw new SyntaxError('Unexpected token in JSON')
|
|
465
|
+
},
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
const { container, application } = installControllerWithStates()
|
|
469
|
+
await flush()
|
|
470
|
+
await flush()
|
|
471
|
+
await flush()
|
|
472
|
+
|
|
473
|
+
expect(container.querySelector('.creative-tree-error')).not.toBeNull()
|
|
474
|
+
expect(container.querySelector('.creative-empty-state')).toBeNull()
|
|
475
|
+
expect(container.querySelector('.new-root-creative-btn')).toBeNull()
|
|
476
|
+
|
|
477
|
+
application.stop()
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
test('the error fallback renders its value as text, never as markup', async () => {
|
|
481
|
+
jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
482
|
+
global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 500, json: async () => ({}) })
|
|
483
|
+
|
|
484
|
+
const { container, application } = installControllerWithStates()
|
|
485
|
+
container.setAttribute(
|
|
486
|
+
'data-creatives--tree-error-text-value',
|
|
487
|
+
'<img src=x onerror="window.__xss = true">'
|
|
488
|
+
)
|
|
489
|
+
await flush()
|
|
490
|
+
await flush()
|
|
491
|
+
await flush()
|
|
492
|
+
|
|
493
|
+
const message = container.querySelector('.creative-tree-error')
|
|
494
|
+
expect(message).not.toBeNull()
|
|
495
|
+
// Parsed as markup this would be an <img>; as text it is just the literal string.
|
|
496
|
+
expect(message.querySelector('img')).toBeNull()
|
|
497
|
+
expect(message.textContent).toBe('<img src=x onerror="window.__xss = true">')
|
|
498
|
+
|
|
499
|
+
application.stop()
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
test('a genuinely empty tree (successful response, zero creatives) still shows the actionable empty state', async () => {
|
|
503
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
504
|
+
ok: true,
|
|
505
|
+
json: async () => ({ creatives: [] }),
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
const { container, application } = installControllerWithStates()
|
|
509
|
+
await flush()
|
|
510
|
+
await flush()
|
|
511
|
+
|
|
512
|
+
expect(container.querySelector('.new-root-creative-btn')).not.toBeNull()
|
|
513
|
+
expect(container.querySelector('.creative-tree-error')).toBeNull()
|
|
514
|
+
|
|
515
|
+
application.stop()
|
|
516
|
+
})
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
// The empty state used to be server-rendered inside #creatives, so the browser
|
|
520
|
+
// painted "No sub-creatives yet" the moment the HTML landed — before Stimulus had
|
|
521
|
+
// booted, let alone before the tree fetch had confirmed anything. index.html.erb
|
|
522
|
+
// now renders the loading placeholder there instead, and the controller adopts
|
|
523
|
+
// that node rather than replacing it.
|
|
524
|
+
describe('CreativesTreeController server-rendered loading placeholder', () => {
|
|
525
|
+
let originalFetch
|
|
526
|
+
|
|
527
|
+
const SERVER_PLACEHOLDER_HTML =
|
|
528
|
+
'<div class="creative-tree-loading-placeholder" data-creatives-tree-loading role="status"' +
|
|
529
|
+
' aria-live="polite" aria-label="크리에이티브를 불러오는 중">' +
|
|
530
|
+
'<span class="creative-loading-indicator" aria-hidden="true">' +
|
|
531
|
+
'<span class="creative-loading-dot">.</span>' +
|
|
532
|
+
'<span class="creative-loading-dot">.</span>' +
|
|
533
|
+
'<span class="creative-loading-dot">.</span>' +
|
|
534
|
+
'</span></div>'
|
|
535
|
+
|
|
536
|
+
const installWithServerPlaceholder = ({ placeholder = true } = {}) => {
|
|
537
|
+
const template = document.createElement('template')
|
|
538
|
+
template.id = 'creatives-empty-state-template'
|
|
539
|
+
template.innerHTML =
|
|
540
|
+
'<div data-creatives-empty-state><div class="creative-empty-state">' +
|
|
541
|
+
'<button class="new-root-creative-btn">Add</button></div></div>'
|
|
542
|
+
document.body.appendChild(template)
|
|
543
|
+
|
|
544
|
+
const container = document.createElement('div')
|
|
545
|
+
container.setAttribute('data-controller', 'creatives--tree')
|
|
546
|
+
container.setAttribute('data-creatives--tree-url-value', '/creatives?format=json&id=991')
|
|
547
|
+
container.setAttribute('data-creatives--tree-loading-text-value', '크리에이티브를 불러오는 중')
|
|
548
|
+
if (placeholder) container.innerHTML = SERVER_PLACEHOLDER_HTML
|
|
549
|
+
document.body.appendChild(container)
|
|
550
|
+
|
|
551
|
+
const serverNode = container.querySelector('[data-creatives-tree-loading]')
|
|
552
|
+
|
|
553
|
+
const application = Application.start()
|
|
554
|
+
application.register('creatives--tree', TreeController)
|
|
555
|
+
|
|
556
|
+
return { container, application, serverNode }
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
beforeEach(() => {
|
|
560
|
+
originalFetch = global.fetch
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
afterEach(() => {
|
|
564
|
+
global.fetch = originalFetch
|
|
565
|
+
document.body.innerHTML = ''
|
|
566
|
+
jest.restoreAllMocks()
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
test('adopts the server-rendered placeholder instead of rebuilding it', async () => {
|
|
570
|
+
let resolveFetch
|
|
571
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { resolveFetch = resolve }))
|
|
572
|
+
|
|
573
|
+
const { container, application, serverNode } = installWithServerPlaceholder()
|
|
574
|
+
await flush()
|
|
575
|
+
|
|
576
|
+
const live = container.querySelector('[data-creatives-tree-loading]')
|
|
577
|
+
// Same node, still attached: rebuilding would blank the container for a frame,
|
|
578
|
+
// which is precisely the flicker this arrangement removes.
|
|
579
|
+
expect(live).toBe(serverNode)
|
|
580
|
+
expect(container.children).toHaveLength(1)
|
|
581
|
+
|
|
582
|
+
resolveFetch({ ok: true, json: async () => ({ creatives: [] }) })
|
|
583
|
+
application.stop()
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
test('does not paint the empty state before the fetch resolves', async () => {
|
|
587
|
+
let resolveFetch
|
|
588
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { resolveFetch = resolve }))
|
|
589
|
+
|
|
590
|
+
const { container, application } = installWithServerPlaceholder()
|
|
591
|
+
await flush()
|
|
592
|
+
|
|
593
|
+
expect(container.querySelector('[data-creatives-empty-state]')).toBeNull()
|
|
594
|
+
expect(container.querySelector('.new-root-creative-btn')).toBeNull()
|
|
595
|
+
expect(container.querySelector('[data-creatives-tree-loading]')).not.toBeNull()
|
|
596
|
+
|
|
597
|
+
resolveFetch({ ok: true, json: async () => ({ creatives: [] }) })
|
|
598
|
+
application.stop()
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
test('swaps the placeholder for the empty state only once zero rows are confirmed', async () => {
|
|
602
|
+
global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ creatives: [] }) })
|
|
603
|
+
|
|
604
|
+
const { container, application } = installWithServerPlaceholder()
|
|
605
|
+
await flush()
|
|
606
|
+
await flush()
|
|
607
|
+
|
|
608
|
+
expect(container.querySelector('[data-creatives-tree-loading]')).toBeNull()
|
|
609
|
+
expect(container.querySelector('.new-root-creative-btn')).not.toBeNull()
|
|
610
|
+
|
|
611
|
+
application.stop()
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
test('re-attaches the adopted placeholder on a later reload', async () => {
|
|
615
|
+
global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ creatives: [] }) })
|
|
616
|
+
|
|
617
|
+
const { container, application, serverNode } = installWithServerPlaceholder()
|
|
618
|
+
await flush()
|
|
619
|
+
await flush()
|
|
620
|
+
expect(container.querySelector('[data-creatives-tree-loading]')).toBeNull()
|
|
621
|
+
|
|
622
|
+
let resolveSecond
|
|
623
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { resolveSecond = resolve }))
|
|
624
|
+
const controller = application.getControllerForElementAndIdentifier(container, 'creatives--tree')
|
|
625
|
+
controller.load()
|
|
626
|
+
|
|
627
|
+
expect(container.querySelector('[data-creatives-tree-loading]')).toBe(serverNode)
|
|
628
|
+
expect(container.querySelector('.new-root-creative-btn')).toBeNull()
|
|
629
|
+
|
|
630
|
+
resolveSecond({ ok: true, json: async () => ({ creatives: [] }) })
|
|
631
|
+
application.stop()
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
test('builds its own placeholder when the container has none, labelled from i18n', async () => {
|
|
635
|
+
let resolveFetch
|
|
636
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { resolveFetch = resolve }))
|
|
637
|
+
|
|
638
|
+
const { container, application } = installWithServerPlaceholder({ placeholder: false })
|
|
639
|
+
await flush()
|
|
640
|
+
|
|
641
|
+
const built = container.querySelector('[data-creatives-tree-loading]')
|
|
642
|
+
expect(built).not.toBeNull()
|
|
643
|
+
expect(built.getAttribute('aria-label')).toBe('크리에이티브를 불러오는 중')
|
|
644
|
+
expect(built.querySelectorAll('.creative-loading-dot')).toHaveLength(3)
|
|
645
|
+
|
|
646
|
+
resolveFetch({ ok: true, json: async () => ({ creatives: [] }) })
|
|
647
|
+
application.stop()
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
test('requires a translated label when no server placeholder is available', async () => {
|
|
651
|
+
let resolveFetch
|
|
652
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { resolveFetch = resolve }))
|
|
653
|
+
|
|
654
|
+
const { container, application } = installWithServerPlaceholder({ placeholder: false })
|
|
655
|
+
await flush()
|
|
656
|
+
container.removeAttribute('data-creatives--tree-loading-text-value')
|
|
657
|
+
container.replaceChildren()
|
|
658
|
+
const controller = application.getControllerForElementAndIdentifier(container, 'creatives--tree')
|
|
659
|
+
controller.loadingIndicator = null
|
|
660
|
+
expect(() => controller.showLoadingIndicator()).toThrow(
|
|
661
|
+
'creatives--tree requires a translated loadingText value when no server loading placeholder is present',
|
|
662
|
+
)
|
|
663
|
+
expect(container.querySelector('[data-creatives-tree-loading]')).toBeNull()
|
|
664
|
+
|
|
665
|
+
resolveFetch({ ok: true, json: async () => ({ creatives: [] }) })
|
|
666
|
+
application.stop()
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
test('never adopts the load-more indicator as the tree placeholder', async () => {
|
|
670
|
+
let resolveFetch
|
|
671
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { resolveFetch = resolve }))
|
|
672
|
+
|
|
673
|
+
const { container, application } = installWithServerPlaceholder({ placeholder: false })
|
|
674
|
+
await flush()
|
|
675
|
+
// The paginated "Chats" feed appends its own .creative-tree-loading-placeholder;
|
|
676
|
+
// it carries no data-creatives-tree-loading, so the selector must miss it.
|
|
677
|
+
const loadMore = document.createElement('div')
|
|
678
|
+
loadMore.className = 'creative-tree-loading-placeholder creative-chats-load-more'
|
|
679
|
+
container.replaceChildren(loadMore)
|
|
680
|
+
|
|
681
|
+
const controller = application.getControllerForElementAndIdentifier(container, 'creatives--tree')
|
|
682
|
+
controller.loadingIndicator = null
|
|
683
|
+
controller.showLoadingIndicator()
|
|
684
|
+
|
|
685
|
+
expect(controller.loadingIndicator).not.toBe(loadMore)
|
|
686
|
+
expect(container.contains(loadMore)).toBe(false)
|
|
687
|
+
|
|
688
|
+
resolveFetch({ ok: true, json: async () => ({ creatives: [] }) })
|
|
689
|
+
application.stop()
|
|
690
|
+
})
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
// A reload replaces the whole container, so it takes the row an open editor is
|
|
694
|
+
// attached to out of the document — along with the unsaved draft inside it.
|
|
695
|
+
// Callers that reload in response to a request they fired earlier (archive /
|
|
696
|
+
// unarchive, delete-with-promoted-children) can land at any moment, including
|
|
697
|
+
// while the user has moved on and is editing a different row, so they go through
|
|
698
|
+
// requestReload() rather than load().
|
|
699
|
+
describe('CreativesTreeController requestReload', () => {
|
|
700
|
+
let originalFetch
|
|
701
|
+
|
|
702
|
+
// Stimulus connects asynchronously, so the controller instance only exists after
|
|
703
|
+
// a turn of the real event loop — hence fake timers are switched on afterwards,
|
|
704
|
+
// once connect()'s own initial load() is out of the way and can be stubbed out.
|
|
705
|
+
const installConnected = async () => {
|
|
706
|
+
const { container, application } = installController()
|
|
707
|
+
await flush()
|
|
708
|
+
const controller = application.getControllerForElementAndIdentifier(container, 'creatives--tree')
|
|
709
|
+
const load = jest.spyOn(controller, 'load').mockImplementation(() => {})
|
|
710
|
+
jest.useFakeTimers()
|
|
711
|
+
return { container, application, controller, load }
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
beforeEach(() => {
|
|
715
|
+
originalFetch = global.fetch
|
|
716
|
+
global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ creatives: [] }) })
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
afterEach(() => {
|
|
720
|
+
jest.useRealTimers()
|
|
721
|
+
global.fetch = originalFetch
|
|
722
|
+
document.body.innerHTML = ''
|
|
723
|
+
jest.restoreAllMocks()
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
test('reloads when no editor is open', async () => {
|
|
727
|
+
const { application, controller, load } = await installConnected()
|
|
728
|
+
|
|
729
|
+
controller.requestReload()
|
|
730
|
+
jest.advanceTimersByTime(300)
|
|
731
|
+
|
|
732
|
+
expect(load).toHaveBeenCalledTimes(1)
|
|
733
|
+
|
|
734
|
+
application.stop()
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
test('defers the reload while a row is being edited', async () => {
|
|
738
|
+
const { application, controller, load } = await installConnected()
|
|
739
|
+
|
|
740
|
+
document.dispatchEvent(new CustomEvent('creative-editing:start'))
|
|
741
|
+
controller.requestReload()
|
|
742
|
+
jest.advanceTimersByTime(5000)
|
|
743
|
+
|
|
744
|
+
expect(load).not.toHaveBeenCalled()
|
|
745
|
+
|
|
746
|
+
application.stop()
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
test('runs the deferred reload once editing stops', async () => {
|
|
750
|
+
const { application, controller, load } = await installConnected()
|
|
751
|
+
|
|
752
|
+
document.dispatchEvent(new CustomEvent('creative-editing:start'))
|
|
753
|
+
controller.requestReload()
|
|
754
|
+
jest.advanceTimersByTime(5000)
|
|
755
|
+
expect(load).not.toHaveBeenCalled()
|
|
756
|
+
|
|
757
|
+
document.dispatchEvent(new CustomEvent('creative-editing:stop'))
|
|
758
|
+
jest.advanceTimersByTime(300)
|
|
759
|
+
|
|
760
|
+
expect(load).toHaveBeenCalledTimes(1)
|
|
761
|
+
|
|
762
|
+
application.stop()
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
test('does not reload on editing:stop when nothing was requested', async () => {
|
|
766
|
+
const { application, controller, load } = await installConnected()
|
|
767
|
+
|
|
768
|
+
document.dispatchEvent(new CustomEvent('creative-editing:start'))
|
|
769
|
+
document.dispatchEvent(new CustomEvent('creative-editing:stop'))
|
|
770
|
+
jest.advanceTimersByTime(5000)
|
|
771
|
+
|
|
772
|
+
expect(load).not.toHaveBeenCalled()
|
|
773
|
+
expect(controller).toBeDefined()
|
|
774
|
+
|
|
775
|
+
application.stop()
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
// Switching rows is a stop immediately followed by a start, so a refetch drained
|
|
779
|
+
// on the stop is still sitting in the 300ms debounce when the next row opens.
|
|
780
|
+
// Checking _editing only at requestReload() time would let that timer fire and
|
|
781
|
+
// re-render the tree out from under the newly opened editor, taking the row it
|
|
782
|
+
// is attached to — and the draft inside it — out of the document.
|
|
783
|
+
test('keeps the deferred reload pending across a row switch', async () => {
|
|
784
|
+
const { application, controller, load } = await installConnected()
|
|
785
|
+
|
|
786
|
+
document.dispatchEvent(new CustomEvent('creative-editing:start'))
|
|
787
|
+
controller.requestReload()
|
|
788
|
+
|
|
789
|
+
// The switch: the outgoing row stops, the incoming row starts, both well
|
|
790
|
+
// inside the debounce window.
|
|
791
|
+
document.dispatchEvent(new CustomEvent('creative-editing:stop'))
|
|
792
|
+
document.dispatchEvent(new CustomEvent('creative-editing:start'))
|
|
793
|
+
jest.advanceTimersByTime(5000)
|
|
794
|
+
|
|
795
|
+
expect(load).not.toHaveBeenCalled()
|
|
796
|
+
|
|
797
|
+
application.stop()
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
test('runs the reload once the row opened by the switch is closed', async () => {
|
|
801
|
+
const { application, controller, load } = await installConnected()
|
|
802
|
+
|
|
803
|
+
document.dispatchEvent(new CustomEvent('creative-editing:start'))
|
|
804
|
+
controller.requestReload()
|
|
805
|
+
document.dispatchEvent(new CustomEvent('creative-editing:stop'))
|
|
806
|
+
document.dispatchEvent(new CustomEvent('creative-editing:start'))
|
|
807
|
+
jest.advanceTimersByTime(5000)
|
|
808
|
+
expect(load).not.toHaveBeenCalled()
|
|
809
|
+
|
|
810
|
+
document.dispatchEvent(new CustomEvent('creative-editing:stop'))
|
|
811
|
+
jest.advanceTimersByTime(300)
|
|
812
|
+
|
|
813
|
+
// Still exactly once: re-pending must not double-book the refetch.
|
|
814
|
+
expect(load).toHaveBeenCalledTimes(1)
|
|
815
|
+
|
|
816
|
+
application.stop()
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
// The re-check must not swallow the request: an editor that opens after the
|
|
820
|
+
// timer has already fired is a separate matter, but one that opens during the
|
|
821
|
+
// window has to leave the refetch queued rather than cancelled.
|
|
822
|
+
test('re-arms the pending flag when the timer fires mid-edit', async () => {
|
|
823
|
+
const { application, controller, load } = await installConnected()
|
|
824
|
+
|
|
825
|
+
controller.requestReload()
|
|
826
|
+
document.dispatchEvent(new CustomEvent('creative-editing:start'))
|
|
827
|
+
jest.advanceTimersByTime(300)
|
|
828
|
+
expect(load).not.toHaveBeenCalled()
|
|
829
|
+
|
|
830
|
+
document.dispatchEvent(new CustomEvent('creative-editing:stop'))
|
|
831
|
+
jest.advanceTimersByTime(300)
|
|
832
|
+
|
|
833
|
+
expect(load).toHaveBeenCalledTimes(1)
|
|
834
|
+
|
|
835
|
+
application.stop()
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
test('keeps a pending reload held after editing stops until the operation finishes', async () => {
|
|
839
|
+
const { application, controller, load } = await installConnected()
|
|
840
|
+
|
|
841
|
+
document.dispatchEvent(new CustomEvent('creative-editing:start'))
|
|
842
|
+
controller.beginReloadHold()
|
|
843
|
+
controller.requestReload()
|
|
844
|
+
document.dispatchEvent(new CustomEvent('creative-editing:stop'))
|
|
845
|
+
jest.advanceTimersByTime(5000)
|
|
846
|
+
|
|
847
|
+
expect(load).not.toHaveBeenCalled()
|
|
848
|
+
|
|
849
|
+
controller.endReloadHold()
|
|
850
|
+
jest.advanceTimersByTime(300)
|
|
851
|
+
|
|
852
|
+
expect(load).toHaveBeenCalledTimes(1)
|
|
853
|
+
|
|
854
|
+
application.stop()
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
test('coalesces reloads across overlapping operation holds', async () => {
|
|
858
|
+
const { application, controller, load } = await installConnected()
|
|
859
|
+
|
|
860
|
+
controller.beginReloadHold()
|
|
861
|
+
controller.beginReloadHold()
|
|
862
|
+
controller.requestReload()
|
|
863
|
+
jest.advanceTimersByTime(5000)
|
|
864
|
+
|
|
865
|
+
controller.endReloadHold()
|
|
866
|
+
jest.advanceTimersByTime(5000)
|
|
867
|
+
expect(load).not.toHaveBeenCalled()
|
|
868
|
+
|
|
869
|
+
controller.endReloadHold()
|
|
870
|
+
jest.advanceTimersByTime(300)
|
|
871
|
+
expect(load).toHaveBeenCalledTimes(1)
|
|
872
|
+
|
|
873
|
+
application.stop()
|
|
874
|
+
})
|
|
270
875
|
})
|