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
|
@@ -52,9 +52,13 @@ module Collavre
|
|
|
52
52
|
elsif can_feedback
|
|
53
53
|
origin = creative.effective_origin
|
|
54
54
|
comments_count = origin.comments_count
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
# A batched count already has presence suppression applied by
|
|
56
|
+
# CommentBadgeIndex; re-checking here would be one cache read per node,
|
|
57
|
+
# which is exactly what the batch exists to avoid.
|
|
58
|
+
if unread_count.nil?
|
|
59
|
+
badge_index = Creatives::CommentBadgeIndex.new(user: Current.user)
|
|
60
|
+
badge_index.index([ origin ])
|
|
61
|
+
unread_count = badge_index.unread_count_for(origin)
|
|
58
62
|
end
|
|
59
63
|
classes = [ "comments-btn", "creative-action-btn" ]
|
|
60
64
|
classes << "no-comments" if comments_count.zero?
|
|
@@ -82,11 +86,13 @@ module Collavre
|
|
|
82
86
|
end
|
|
83
87
|
is_leaf = has_children.nil? ? !creative.children.exists? : !has_children
|
|
84
88
|
can_write = creative.has_permission?(Current.user, :write) if can_write.nil?
|
|
85
|
-
progress_part =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
progress_part = render_progress_control(
|
|
90
|
+
creative,
|
|
91
|
+
progress_value,
|
|
92
|
+
has_children: !is_leaf,
|
|
93
|
+
can_write: can_write,
|
|
94
|
+
select_mode: select_mode
|
|
95
|
+
)
|
|
90
96
|
|
|
91
97
|
safe_join([
|
|
92
98
|
progress_part,
|
|
@@ -97,17 +103,18 @@ module Collavre
|
|
|
97
103
|
end
|
|
98
104
|
end
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return comments_count unless last_read_id
|
|
107
|
-
|
|
108
|
-
origin.comments.where("id > ? and private = ?", last_read_id, false).count
|
|
106
|
+
def render_progress_control(creative, value, has_children:, can_write:, select_mode: false)
|
|
107
|
+
if !has_children && can_write && !select_mode && progress_toggleable?(value)
|
|
108
|
+
render_progress_toggle(creative, value)
|
|
109
|
+
else
|
|
110
|
+
render_progress_value(value)
|
|
111
|
+
end
|
|
109
112
|
end
|
|
110
113
|
|
|
114
|
+
# Rendered when the completion mark is blank, so the completed state keeps a
|
|
115
|
+
# stable baseline and a tappable hit area.
|
|
116
|
+
NBSP = "\u00A0"
|
|
117
|
+
|
|
111
118
|
def render_progress_toggle(creative, value)
|
|
112
119
|
complete = value == 1
|
|
113
120
|
new_value = complete ? 0 : 1
|
|
@@ -118,8 +125,13 @@ module Collavre
|
|
|
118
125
|
data: {
|
|
119
126
|
progress_toggle: true,
|
|
120
127
|
creative_id: creative.id,
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
# progress is a decimal, so the raw value serializes as "1.0" and the
|
|
129
|
+
# CSS/JS state checks against "1" would never match. Both sides of the
|
|
130
|
+
# toggle only ever mean 0 or 1, so emit it as an integer.
|
|
131
|
+
current_progress: complete ? 1 : 0,
|
|
132
|
+
new_progress: new_value,
|
|
133
|
+
mark_complete: t("collavre.creatives.index.mark_complete"),
|
|
134
|
+
mark_incomplete: t("collavre.creatives.index.mark_incomplete")
|
|
123
135
|
},
|
|
124
136
|
title: tooltip
|
|
125
137
|
) do
|
|
@@ -127,16 +139,29 @@ module Collavre
|
|
|
127
139
|
type: "checkbox",
|
|
128
140
|
checked: complete || nil,
|
|
129
141
|
class: "progress-toggle-checkbox",
|
|
130
|
-
tabindex: -1,
|
|
131
142
|
"aria-label": tooltip
|
|
132
143
|
)
|
|
133
|
-
|
|
144
|
+
# A completed leaf reads as the admin completion mark (blank by default),
|
|
145
|
+
# the same way parent rows already render 100%. CSS swaps the mark back to
|
|
146
|
+
# the checked box on hover/focus so a mis-click is undone in place instead
|
|
147
|
+
# of through the inline editor. The checkbox stays the accessible control;
|
|
148
|
+
# the mark is decorative.
|
|
149
|
+
checkbox + tag.span(completion_mark_display, class: "progress-toggle-mark", aria: { hidden: true })
|
|
134
150
|
end
|
|
135
151
|
end
|
|
136
152
|
|
|
153
|
+
# Blank (the default) collapses to a non-breaking space so the completed state
|
|
154
|
+
# keeps a stable baseline and a tappable hit area inside the toggle.
|
|
155
|
+
def completion_mark_display
|
|
156
|
+
completion_mark.to_s.presence || NBSP
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def progress_toggleable?(value)
|
|
160
|
+
value == 0 || value == 1
|
|
161
|
+
end
|
|
162
|
+
|
|
137
163
|
def render_progress_value(value)
|
|
138
164
|
text = number_to_percentage(value * 100, precision: 0)
|
|
139
|
-
completion_mark = Collavre::SystemSetting.completion_mark
|
|
140
165
|
if value == 1 && !completion_mark.nil?
|
|
141
166
|
text = completion_mark
|
|
142
167
|
end
|
|
@@ -148,6 +173,15 @@ module Collavre
|
|
|
148
173
|
)
|
|
149
174
|
end
|
|
150
175
|
|
|
176
|
+
# A tree renders this helper once per node. Keep the setting lookup scoped to
|
|
177
|
+
# the request's view context rather than depending on a particular cache
|
|
178
|
+
# store's local-cache middleware to collapse repeated reads.
|
|
179
|
+
def completion_mark
|
|
180
|
+
return @completion_mark if defined?(@completion_mark)
|
|
181
|
+
|
|
182
|
+
@completion_mark = Collavre::SystemSetting.completion_mark
|
|
183
|
+
end
|
|
184
|
+
|
|
151
185
|
def render_creative_tree_markdown(creatives, level = 1, with_progress = false, max_depth: nil)
|
|
152
186
|
return "" if creatives.blank?
|
|
153
187
|
md = ""
|
|
@@ -61,6 +61,18 @@ module Collavre
|
|
|
61
61
|
value.is_a?(Proc) ? instance_exec(&value) : value
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# Destination for the Home button.
|
|
65
|
+
#
|
|
66
|
+
# Authenticated users are sent to the configured home page directly instead
|
|
67
|
+
# of through "/". "/" means "app launch" and is rewritten to the last
|
|
68
|
+
# visited Creative, which made Home a no-op for anyone who had opened one.
|
|
69
|
+
# Guests keep "/" so the middleware rewrite still applies.
|
|
70
|
+
def home_navigation_path
|
|
71
|
+
return main_app.root_path unless authenticated?
|
|
72
|
+
|
|
73
|
+
HomePagePath.absolute(script_name: request.script_name) || main_app.root_path
|
|
74
|
+
end
|
|
75
|
+
|
|
64
76
|
private
|
|
65
77
|
|
|
66
78
|
def render_nav_button(item)
|
data/app/javascript/collavre.js
CHANGED
|
@@ -5,12 +5,10 @@
|
|
|
5
5
|
import "./modules/creatives"
|
|
6
6
|
import "./modules/creative_row_swipe"
|
|
7
7
|
import "./modules/mention_menu"
|
|
8
|
-
import "./modules/typo_corrector"
|
|
9
8
|
import "./modules/command_menu"
|
|
10
9
|
import "./modules/modal_dialog"
|
|
11
10
|
import "./modules/export_to_markdown"
|
|
12
11
|
import "./modules/inbox_panel"
|
|
13
|
-
import "./modules/creative_guide"
|
|
14
12
|
import "./modules/creative_row_editor"
|
|
15
13
|
import "./modules/slide_view"
|
|
16
14
|
|
|
@@ -19,6 +17,7 @@ import "./components/creative_tree_row"
|
|
|
19
17
|
|
|
20
18
|
// Import and re-export lib utilities
|
|
21
19
|
import "./lib/apply_lexical_styles"
|
|
20
|
+
import "./lib/creative_link_prefetch"
|
|
22
21
|
import "./lib/turbo_stream_actions"
|
|
23
22
|
import "./lib/turbo_confirm"
|
|
24
23
|
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
registerCodeHighlighting
|
|
20
20
|
} from "@lexical/code"
|
|
21
21
|
import { ListItemNode, ListNode, $isListItemNode, $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from "@lexical/list"
|
|
22
|
-
import {
|
|
22
|
+
import { LinkNode, AutoLinkNode } from "@lexical/link"
|
|
23
23
|
import { TableNode, TableRowNode, TableCellNode, INSERT_TABLE_COMMAND } from "@lexical/table"
|
|
24
24
|
import { TablePlugin } from "@lexical/react/LexicalTablePlugin"
|
|
25
25
|
import TableHoverActionsPlugin from "./plugins/table_hover_actions_plugin"
|
|
@@ -68,6 +68,9 @@ import { $convertToMarkdownString } from "@lexical/markdown"
|
|
|
68
68
|
import { updateResponsiveImages } from "../lib/responsive_images"
|
|
69
69
|
import { CODE_TOKEN_THEME } from "../lib/editor/code_token_theme"
|
|
70
70
|
import { detectCodeLanguage, normalizeFenceLang, bridgeCodeFenceLanguages, markLanguageResolved, isLanguageResolved, clearLanguageResolved } from "../lib/editor/code_languages"
|
|
71
|
+
import { CreativeLinkNode } from "../lib/lexical/creative_link_node"
|
|
72
|
+
import { registerCreativeLinkTrigger } from "../lib/lexical/creative_link_trigger"
|
|
73
|
+
import { $createToolbarLinkNode, $findLinkNode } from "../lib/lexical/link_toolbar"
|
|
71
74
|
|
|
72
75
|
const URL_MATCHERS = [
|
|
73
76
|
createLinkMatcherWithRegExp(/https?:\/\/[^\s<]+/gi, (text) => text)
|
|
@@ -292,6 +295,24 @@ function LinkAttributesPlugin() {
|
|
|
292
295
|
return null
|
|
293
296
|
}
|
|
294
297
|
|
|
298
|
+
function CreativeLinkTriggerPlugin() {
|
|
299
|
+
const [editor] = useLexicalComposerContext()
|
|
300
|
+
|
|
301
|
+
useEffect(() => registerCreativeLinkTrigger(editor, ({ anchorRect, onSelect, onClose }) => {
|
|
302
|
+
const modal = document.getElementById("link-creative-modal")
|
|
303
|
+
const controller = modal && window.Stimulus?.getControllerForElementAndIdentifier(
|
|
304
|
+
modal,
|
|
305
|
+
"link-creative"
|
|
306
|
+
)
|
|
307
|
+
if (!controller) return false
|
|
308
|
+
|
|
309
|
+
controller.open(anchorRect, onSelect, onClose, { allowCreate: true })
|
|
310
|
+
return true
|
|
311
|
+
}), [editor])
|
|
312
|
+
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
|
|
295
316
|
function CodeHighlightingPlugin() {
|
|
296
317
|
const [editor] = useLexicalComposerContext()
|
|
297
318
|
|
|
@@ -559,7 +580,6 @@ function Toolbar() {
|
|
|
559
580
|
}, [editor])
|
|
560
581
|
|
|
561
582
|
const toggleLink = useCallback(() => {
|
|
562
|
-
let hasLink = false
|
|
563
583
|
let selectionText = ""
|
|
564
584
|
let isRange = false
|
|
565
585
|
let url = ""
|
|
@@ -571,17 +591,10 @@ function Toolbar() {
|
|
|
571
591
|
isRange = true
|
|
572
592
|
selectionText = selection.getTextContent()
|
|
573
593
|
|
|
574
|
-
const
|
|
575
|
-
const nodeWithLink = nodes.find((node) => {
|
|
576
|
-
if (node.getType() === "link") return true
|
|
577
|
-
const parent = node.getParent()
|
|
578
|
-
return parent?.getType() === "link"
|
|
579
|
-
})
|
|
594
|
+
const nodeWithLink = $findLinkNode(selection.getNodes())
|
|
580
595
|
|
|
581
596
|
if (nodeWithLink) {
|
|
582
|
-
|
|
583
|
-
const linkNode = nodeWithLink.getType() === "link" ? nodeWithLink : nodeWithLink.getParent()
|
|
584
|
-
url = linkNode.getURL()
|
|
597
|
+
url = nodeWithLink.getURL()
|
|
585
598
|
}
|
|
586
599
|
})
|
|
587
600
|
|
|
@@ -860,23 +873,17 @@ function Toolbar() {
|
|
|
860
873
|
if (!selection) return
|
|
861
874
|
|
|
862
875
|
// Check if we are editing an existing link
|
|
863
|
-
const
|
|
864
|
-
const nodeWithLink = nodes.find((node) => {
|
|
865
|
-
if (node.getType() === "link") return true
|
|
866
|
-
const parent = node.getParent()
|
|
867
|
-
return parent?.getType() === "link"
|
|
868
|
-
})
|
|
876
|
+
const nodeWithLink = $findLinkNode(selection.getNodes())
|
|
869
877
|
|
|
870
878
|
if (nodeWithLink) {
|
|
871
879
|
// UPDATE MODE
|
|
872
|
-
const
|
|
873
|
-
const newLink = $createLinkNode(finalUrl)
|
|
880
|
+
const newLink = $createToolbarLinkNode(finalUrl)
|
|
874
881
|
newLink.append($createTextNode(finalLabel))
|
|
875
|
-
|
|
882
|
+
nodeWithLink.replace(newLink)
|
|
876
883
|
newLink.select()
|
|
877
884
|
} else {
|
|
878
885
|
// CREATE MODE
|
|
879
|
-
const newLink = $
|
|
886
|
+
const newLink = $createToolbarLinkNode(finalUrl)
|
|
880
887
|
newLink.append($createTextNode(finalLabel))
|
|
881
888
|
|
|
882
889
|
if ($isRangeSelection(selection) && !selection.isCollapsed()) {
|
|
@@ -1013,6 +1020,7 @@ function EditorInner({
|
|
|
1013
1020
|
<TrailingParagraphPlugin />
|
|
1014
1021
|
<InitialContentPlugin html={initialHtml} />
|
|
1015
1022
|
<LinkAttributesPlugin />
|
|
1023
|
+
<CreativeLinkTriggerPlugin />
|
|
1016
1024
|
<ReadyPlugin onReady={onReady} />
|
|
1017
1025
|
<FileUploadPlugin
|
|
1018
1026
|
onUploadStateChange={onUploadStateChange}
|
|
@@ -1081,6 +1089,7 @@ export default function InlineLexicalEditor({
|
|
|
1081
1089
|
ListNode,
|
|
1082
1090
|
LinkNode,
|
|
1083
1091
|
AutoLinkNode,
|
|
1092
|
+
CreativeLinkNode,
|
|
1084
1093
|
ImageNode,
|
|
1085
1094
|
AttachmentNode,
|
|
1086
1095
|
VideoNode,
|
|
@@ -14,17 +14,19 @@
|
|
|
14
14
|
// The component calls `customElements.define(...)` at module-eval time and reads
|
|
15
15
|
// `customElements` as a bare global. The jest environment only exposes it on
|
|
16
16
|
// `window`, so bridge it before importing the component.
|
|
17
|
+
import { jest } from '@jest/globals'
|
|
18
|
+
|
|
17
19
|
beforeAll(() => {
|
|
18
20
|
if (typeof globalThis.customElements === "undefined") {
|
|
19
21
|
globalThis.customElements = window.customElements;
|
|
20
22
|
}
|
|
21
23
|
});
|
|
22
24
|
|
|
23
|
-
async function mountRow(props) {
|
|
25
|
+
async function mountRow(props, parent = document.body) {
|
|
24
26
|
await import("../creative_tree_row.js");
|
|
25
27
|
const el = document.createElement("creative-tree-row");
|
|
26
28
|
Object.assign(el, props);
|
|
27
|
-
|
|
29
|
+
parent.appendChild(el);
|
|
28
30
|
await el.updateComplete;
|
|
29
31
|
return el;
|
|
30
32
|
}
|
|
@@ -39,6 +41,17 @@ afterEach(() => {
|
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
describe("creative-tree-row parentId convention", () => {
|
|
44
|
+
test("uses the shared chevrons for collapsed and expanded branches", async () => {
|
|
45
|
+
const el = await mountRow({ creativeId: "8", hasChildren: true, expanded: true });
|
|
46
|
+
|
|
47
|
+
expect(el.querySelector(".creative-toggle-btn path").getAttribute("d")).toBe("M6 9L12 15L18 9");
|
|
48
|
+
|
|
49
|
+
el.expanded = false;
|
|
50
|
+
await el.updateComplete;
|
|
51
|
+
|
|
52
|
+
expect(el.querySelector(".creative-toggle-btn path").getAttribute("d")).toBe("M9 6L15 12L9 18");
|
|
53
|
+
});
|
|
54
|
+
|
|
42
55
|
test("non-title row under a page-title parent keeps the parent id", async () => {
|
|
43
56
|
const el = await mountRow({ creativeId: "8", parentId: "5", level: 2 });
|
|
44
57
|
const tree = treeOf(el);
|
|
@@ -84,4 +97,21 @@ describe("creative-tree-row parentId convention", () => {
|
|
|
84
97
|
const derivedParentId = prev.dataset.parentId || "";
|
|
85
98
|
expect(derivedParentId).toBe("5"); // sibling inherits the page-title parent
|
|
86
99
|
});
|
|
100
|
+
|
|
101
|
+
test("content navigation stays inside the persistent workspace frame", async () => {
|
|
102
|
+
const frame = document.createElement("turbo-frame");
|
|
103
|
+
frame.id = "creative-workspace-content";
|
|
104
|
+
document.body.appendChild(frame);
|
|
105
|
+
const visit = jest.fn();
|
|
106
|
+
window.Turbo = { visit };
|
|
107
|
+
const el = await mountRow({ creativeId: "8", linkUrl: "/creatives/8" }, frame);
|
|
108
|
+
|
|
109
|
+
el.querySelector(".creative-content").dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
|
|
110
|
+
|
|
111
|
+
expect(visit).toHaveBeenCalledWith("/creatives/8", {
|
|
112
|
+
action: "advance",
|
|
113
|
+
frame: "creative-workspace-content",
|
|
114
|
+
});
|
|
115
|
+
delete window.Turbo;
|
|
116
|
+
});
|
|
87
117
|
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { jest } from '@jest/globals'
|
|
5
|
+
|
|
6
|
+
const csrfFetch = jest.fn()
|
|
7
|
+
|
|
8
|
+
jest.unstable_mockModule('../../lib/api/csrf_fetch', () => ({
|
|
9
|
+
default: csrfFetch,
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
if (typeof globalThis.customElements === 'undefined') {
|
|
14
|
+
globalThis.customElements = window.customElements
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
async function mountRow(progressHtml) {
|
|
19
|
+
await import('../creative_tree_row.js')
|
|
20
|
+
const row = document.createElement('creative-tree-row')
|
|
21
|
+
row.creativeId = '7'
|
|
22
|
+
row.progressHtml = progressHtml
|
|
23
|
+
document.body.appendChild(row)
|
|
24
|
+
await row.updateComplete
|
|
25
|
+
return row
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const INCOMPLETE_TOGGLE = '<span class="progress-toggle-wrap" data-progress-toggle="true" data-creative-id="7" data-current-progress="0" data-new-progress="1" data-mark-complete="Mark complete" data-mark-incomplete="Mark incomplete" title="Mark complete"><input type="checkbox" class="progress-toggle-checkbox" aria-label="Mark complete"><span class="progress-toggle-mark" aria-hidden="true"> </span></span>'
|
|
29
|
+
|
|
30
|
+
const COMPLETE_TOGGLE = INCOMPLETE_TOGGLE
|
|
31
|
+
.replace('data-current-progress="0" data-new-progress="1"', 'data-current-progress="1" data-new-progress="0"')
|
|
32
|
+
.replace('title="Mark complete"', 'title="Mark incomplete"')
|
|
33
|
+
.replace('<input type="checkbox"', '<input type="checkbox" checked')
|
|
34
|
+
.replace('aria-label="Mark complete"', 'aria-label="Mark incomplete"')
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
csrfFetch.mockReset()
|
|
38
|
+
document.body.innerHTML = ''
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('updates checkbox metadata and ancestor progress after a successful toggle', async () => {
|
|
42
|
+
csrfFetch.mockResolvedValue({
|
|
43
|
+
ok: true,
|
|
44
|
+
json: async () => ({
|
|
45
|
+
progress: 1,
|
|
46
|
+
progress_html: INCOMPLETE_TOGGLE.replace('data-current-progress="0"', 'data-current-progress="1"'),
|
|
47
|
+
has_children: false,
|
|
48
|
+
ancestors: [{
|
|
49
|
+
id: '42',
|
|
50
|
+
progress: 0.5,
|
|
51
|
+
progress_html: '<span class="creative-progress-incomplete">50%</span>',
|
|
52
|
+
}],
|
|
53
|
+
}),
|
|
54
|
+
})
|
|
55
|
+
const ancestor = document.createElement('creative-tree-row')
|
|
56
|
+
ancestor.setAttribute('creative-id', '42')
|
|
57
|
+
document.body.appendChild(ancestor)
|
|
58
|
+
const row = await mountRow(INCOMPLETE_TOGGLE)
|
|
59
|
+
|
|
60
|
+
row.querySelector('[data-progress-toggle]').click()
|
|
61
|
+
await Promise.resolve()
|
|
62
|
+
await row.updateComplete
|
|
63
|
+
|
|
64
|
+
expect(csrfFetch).toHaveBeenCalledWith('/creatives/7', expect.objectContaining({ method: 'PATCH' }))
|
|
65
|
+
expect(row.dataset.progressValue).toBe('1')
|
|
66
|
+
expect(ancestor.progressHtml).toContain('50%')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('flips the completion state optimistically so the mark shows before the request settles', async () => {
|
|
70
|
+
let resolveFetch
|
|
71
|
+
csrfFetch.mockReturnValue(new Promise((resolve) => { resolveFetch = resolve }))
|
|
72
|
+
const row = await mountRow(INCOMPLETE_TOGGLE)
|
|
73
|
+
const toggle = row.querySelector('[data-progress-toggle]')
|
|
74
|
+
|
|
75
|
+
toggle.click()
|
|
76
|
+
|
|
77
|
+
// CSS keys the mark/checkbox swap off data-current-progress, so the completed
|
|
78
|
+
// row reads as done immediately instead of waiting on the PATCH.
|
|
79
|
+
expect(toggle.dataset.currentProgress).toBe('1')
|
|
80
|
+
expect(toggle.dataset.newProgress).toBe('0')
|
|
81
|
+
expect(toggle.title).toBe('Mark incomplete')
|
|
82
|
+
expect(toggle.querySelector('.progress-toggle-mark')).not.toBeNull()
|
|
83
|
+
|
|
84
|
+
resolveFetch({ ok: true, json: async () => ({ progress: 1 }) })
|
|
85
|
+
await Promise.resolve()
|
|
86
|
+
await row.updateComplete
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Space (and a click landing on the box itself) runs the input's own activation:
|
|
90
|
+
// the browser flips `checked` before the click reaches this handler. Reading the
|
|
91
|
+
// checkbox instead of the dataset would invert the state back while the PATCH is
|
|
92
|
+
// in flight, so a completed row would still read as checked. `checkbox.click()`
|
|
93
|
+
// drives the same activation path Space does — jsdom has no Space-to-click
|
|
94
|
+
// default action. Its canceled-activation restore inverts the current value
|
|
95
|
+
// where browsers reassign the saved one, so the browser-faithful check lives in
|
|
96
|
+
// test/system/creative_progress_toggle_keyboard_test.rb.
|
|
97
|
+
test('unchecks the box when the completed checkbox itself is activated', async () => {
|
|
98
|
+
let resolveFetch
|
|
99
|
+
csrfFetch.mockReturnValue(new Promise((resolve) => { resolveFetch = resolve }))
|
|
100
|
+
const row = await mountRow(COMPLETE_TOGGLE)
|
|
101
|
+
const toggle = row.querySelector('[data-progress-toggle]')
|
|
102
|
+
const checkbox = toggle.querySelector('input')
|
|
103
|
+
|
|
104
|
+
checkbox.click()
|
|
105
|
+
|
|
106
|
+
expect(toggle.dataset.currentProgress).toBe('0')
|
|
107
|
+
expect(toggle.dataset.newProgress).toBe('1')
|
|
108
|
+
expect(checkbox.checked).toBe(false)
|
|
109
|
+
|
|
110
|
+
resolveFetch({ ok: true, json: async () => ({ progress: 0 }) })
|
|
111
|
+
await Promise.resolve()
|
|
112
|
+
await row.updateComplete
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('rolls the box back to unchecked when activating it directly fails', async () => {
|
|
116
|
+
jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
117
|
+
csrfFetch.mockResolvedValue({ ok: false, status: 422 })
|
|
118
|
+
const row = await mountRow(INCOMPLETE_TOGGLE)
|
|
119
|
+
const toggle = row.querySelector('[data-progress-toggle]')
|
|
120
|
+
const checkbox = toggle.querySelector('input')
|
|
121
|
+
|
|
122
|
+
checkbox.click()
|
|
123
|
+
await Promise.resolve()
|
|
124
|
+
await row.updateComplete
|
|
125
|
+
|
|
126
|
+
expect(toggle.dataset.currentProgress).toBe('0')
|
|
127
|
+
expect(checkbox.checked).toBe(false)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// `pointer-events: none` on the saving row stops the mouse but not a checkbox
|
|
131
|
+
// that already holds focus, so Space can re-enter the handler with the
|
|
132
|
+
// optimistically inverted dataset and PATCH the opposite value. Two requests in
|
|
133
|
+
// flight can settle out of order and leave the row on the losing one.
|
|
134
|
+
test('ignores repeat activation while the PATCH is in flight', async () => {
|
|
135
|
+
csrfFetch.mockReturnValue(new Promise(() => {}))
|
|
136
|
+
const row = await mountRow(COMPLETE_TOGGLE)
|
|
137
|
+
const toggle = row.querySelector('[data-progress-toggle]')
|
|
138
|
+
const checkbox = toggle.querySelector('input')
|
|
139
|
+
|
|
140
|
+
checkbox.click()
|
|
141
|
+
checkbox.click()
|
|
142
|
+
|
|
143
|
+
expect(csrfFetch).toHaveBeenCalledTimes(1)
|
|
144
|
+
expect(toggle.dataset.currentProgress).toBe('0')
|
|
145
|
+
expect(toggle.dataset.newProgress).toBe('1')
|
|
146
|
+
expect(checkbox.checked).toBe(false)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// The server's progress_html replaces the toggle through unsafeHTML, which
|
|
150
|
+
// discards the focused checkbox. Without restoring focus a keyboard user
|
|
151
|
+
// toggles once and then Space scrolls the page instead of toggling back.
|
|
152
|
+
test('keeps focus on the checkbox after the server re-renders the toggle', async () => {
|
|
153
|
+
csrfFetch.mockResolvedValue({
|
|
154
|
+
ok: true,
|
|
155
|
+
json: async () => ({ progress: 1, progress_html: COMPLETE_TOGGLE }),
|
|
156
|
+
})
|
|
157
|
+
const row = await mountRow(INCOMPLETE_TOGGLE)
|
|
158
|
+
const checkbox = row.querySelector('.progress-toggle-checkbox')
|
|
159
|
+
checkbox.focus()
|
|
160
|
+
|
|
161
|
+
checkbox.click()
|
|
162
|
+
await Promise.resolve()
|
|
163
|
+
await row.updateComplete
|
|
164
|
+
await row.updateComplete
|
|
165
|
+
|
|
166
|
+
const rendered = row.querySelector('.progress-toggle-checkbox')
|
|
167
|
+
expect(document.activeElement).toBe(rendered)
|
|
168
|
+
expect(rendered.checked).toBe(true)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('restores checkbox metadata when a toggle request fails', async () => {
|
|
172
|
+
jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
173
|
+
csrfFetch.mockResolvedValue({ ok: false, status: 422 })
|
|
174
|
+
const row = await mountRow(INCOMPLETE_TOGGLE)
|
|
175
|
+
const toggle = row.querySelector('[data-progress-toggle]')
|
|
176
|
+
|
|
177
|
+
toggle.click()
|
|
178
|
+
await Promise.resolve()
|
|
179
|
+
await row.updateComplete
|
|
180
|
+
|
|
181
|
+
expect(toggle.dataset.currentProgress).toBe('0')
|
|
182
|
+
expect(toggle.dataset.newProgress).toBe('1')
|
|
183
|
+
expect(toggle.title).toBe('Mark complete')
|
|
184
|
+
expect(toggle.querySelector('input').getAttribute('aria-label')).toBe('Mark complete')
|
|
185
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { LitElement, html,
|
|
1
|
+
import { LitElement, html, nothing } from "lit";
|
|
2
2
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
3
|
+
import { CHEVRON_COLLAPSED, CHEVRON_EXPANDED } from "../utils/chevron_icons";
|
|
3
4
|
import { parseEmojis } from "../utils/emoji_parser";
|
|
4
5
|
import { highlightCodeBlocks } from "../lib/utils/markdown";
|
|
5
6
|
import { addCreativeTableDownloadButtons } from "../lib/utils/table_download";
|
|
@@ -77,9 +78,25 @@ class CreativeTreeRow extends LitElement {
|
|
|
77
78
|
this._extractTemplates();
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
// `progressHtml` is committed through unsafeHTML, so every render that changes
|
|
82
|
+
// it discards the toggle — including the broadcast that lands after the PATCH
|
|
83
|
+
// this row already applied. A keyboard user would toggle once and then Space
|
|
84
|
+
// would scroll the page, so remember focus here and restore it in updated().
|
|
85
|
+
update(changedProperties) {
|
|
86
|
+
this._progressHadFocus = this._progressToggle != null &&
|
|
87
|
+
this._progressToggle.contains(document.activeElement);
|
|
88
|
+
super.update(changedProperties);
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
updated(changedProperties) {
|
|
81
92
|
this._attachHandlers();
|
|
82
93
|
|
|
94
|
+
if (this._progressHadFocus) {
|
|
95
|
+
this._progressHadFocus = false;
|
|
96
|
+
const checkbox = this.querySelector(".progress-toggle-checkbox");
|
|
97
|
+
if (checkbox && checkbox !== document.activeElement) checkbox.focus();
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
// Re-tokenize the server-rendered description code blocks with hljs so they
|
|
84
101
|
// match the editor's palette and follow light/dark theme. Idempotent: only
|
|
85
102
|
// unmarked blocks are processed, and lit re-renders description DOM (dropping
|
|
@@ -485,14 +502,7 @@ class CreativeTreeRow extends LitElement {
|
|
|
485
502
|
}
|
|
486
503
|
|
|
487
504
|
_toggleIcon() {
|
|
488
|
-
|
|
489
|
-
return svg`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
490
|
-
<path d="M6 9L12 15L18 9"/>
|
|
491
|
-
</svg>`;
|
|
492
|
-
}
|
|
493
|
-
return svg`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
494
|
-
<path d="M9 6L15 12L9 18"/>
|
|
495
|
-
</svg>`;
|
|
505
|
+
return unsafeHTML(this.expanded ? CHEVRON_EXPANDED : CHEVRON_COLLAPSED);
|
|
496
506
|
}
|
|
497
507
|
|
|
498
508
|
_handleToggleClick(event) {
|
|
@@ -546,20 +556,45 @@ class CreativeTreeRow extends LitElement {
|
|
|
546
556
|
}
|
|
547
557
|
|
|
548
558
|
async _handleProgressToggle(event) {
|
|
549
|
-
event.preventDefault();
|
|
550
|
-
event.stopPropagation();
|
|
551
559
|
const wrap = event.currentTarget;
|
|
560
|
+
const checkbox = wrap.querySelector(".progress-toggle-checkbox");
|
|
561
|
+
// Activating the input itself (Space, or a click landing on the box) flips
|
|
562
|
+
// `checked` before the click reaches this handler, and preventDefault would
|
|
563
|
+
// flip it back once dispatch finishes. Leave that activation alone and set
|
|
564
|
+
// the state explicitly below; cancel the default for every other target.
|
|
565
|
+
const nativeActivation = checkbox != null && event.target === checkbox;
|
|
566
|
+
if (!nativeActivation) event.preventDefault();
|
|
567
|
+
event.stopPropagation();
|
|
568
|
+
// `pointer-events: none` on the saving row stops the mouse but not a
|
|
569
|
+
// checkbox that already holds focus, so Space can re-enter here while a
|
|
570
|
+
// PATCH is in flight. The dataset is already inverted at that point, so a
|
|
571
|
+
// second request would carry the opposite value and the two could settle
|
|
572
|
+
// out of order. Drop the activation and put the box back on the dataset.
|
|
573
|
+
if (wrap.classList.contains("progress-toggle-saving")) {
|
|
574
|
+
if (checkbox) checkbox.checked = wrap.dataset.currentProgress === "1";
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
552
577
|
const creativeId = wrap.dataset.creativeId;
|
|
553
578
|
const newProgress = wrap.dataset.newProgress;
|
|
554
|
-
|
|
579
|
+
// The dataset is the source of truth: `checked` may already be flipped.
|
|
580
|
+
const wasComplete = wrap.dataset.currentProgress === "1";
|
|
581
|
+
if (!creativeId || newProgress == null) {
|
|
582
|
+
if (checkbox) checkbox.checked = wasComplete;
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
555
585
|
|
|
556
|
-
// Optimistic UI: toggle
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
const
|
|
586
|
+
// Optimistic UI: toggle the always-visible checkbox immediately.
|
|
587
|
+
const previousCurrentProgress = wrap.dataset.currentProgress;
|
|
588
|
+
const previousNewProgress = wrap.dataset.newProgress;
|
|
589
|
+
const previousTitle = wrap.title;
|
|
560
590
|
if (checkbox) checkbox.checked = !wasComplete;
|
|
561
|
-
|
|
562
|
-
|
|
591
|
+
const complete = newProgress === "1";
|
|
592
|
+
wrap.dataset.currentProgress = newProgress;
|
|
593
|
+
wrap.dataset.newProgress = complete ? "0" : "1";
|
|
594
|
+
const label = complete ? wrap.dataset.markIncomplete : wrap.dataset.markComplete;
|
|
595
|
+
if (label) {
|
|
596
|
+
wrap.title = label;
|
|
597
|
+
if (checkbox) checkbox.setAttribute("aria-label", label);
|
|
563
598
|
}
|
|
564
599
|
wrap.classList.add("progress-toggle-saving");
|
|
565
600
|
|
|
@@ -604,9 +639,10 @@ class CreativeTreeRow extends LitElement {
|
|
|
604
639
|
} catch (err) {
|
|
605
640
|
// Revert optimistic UI
|
|
606
641
|
if (checkbox) checkbox.checked = wasComplete;
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
642
|
+
wrap.dataset.currentProgress = previousCurrentProgress;
|
|
643
|
+
wrap.dataset.newProgress = previousNewProgress;
|
|
644
|
+
wrap.title = previousTitle;
|
|
645
|
+
if (checkbox) checkbox.setAttribute("aria-label", previousTitle);
|
|
610
646
|
console.error("Progress toggle failed:", err);
|
|
611
647
|
} finally {
|
|
612
648
|
wrap.classList.remove("progress-toggle-saving");
|
|
@@ -635,7 +671,11 @@ class CreativeTreeRow extends LitElement {
|
|
|
635
671
|
// If not interactive, navigate to the linkUrl
|
|
636
672
|
if (this.linkUrl && this.linkUrl !== "#") {
|
|
637
673
|
if (window.Turbo) {
|
|
638
|
-
|
|
674
|
+
const workspaceFrame = this.closest("turbo-frame#creative-workspace-content");
|
|
675
|
+
const options = workspaceFrame
|
|
676
|
+
? { action: "advance", frame: workspaceFrame.id }
|
|
677
|
+
: undefined;
|
|
678
|
+
window.Turbo.visit(this.linkUrl, options);
|
|
639
679
|
} else {
|
|
640
680
|
window.location.href = this.linkUrl;
|
|
641
681
|
}
|