collavre 0.1.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 +7 -0
- data/README.md +221 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/collavre/actiontext.css +577 -0
- data/app/assets/stylesheets/collavre/activity_logs.css +99 -0
- data/app/assets/stylesheets/collavre/comments_popup.css +692 -0
- data/app/assets/stylesheets/collavre/creatives.css +559 -0
- data/app/assets/stylesheets/collavre/dark_mode.css +118 -0
- data/app/assets/stylesheets/collavre/mention_menu.css +43 -0
- data/app/assets/stylesheets/collavre/popup.css +160 -0
- data/app/assets/stylesheets/collavre/print.css +37 -0
- data/app/assets/stylesheets/collavre/slide_view.css +79 -0
- data/app/assets/stylesheets/collavre/user_menu.css +34 -0
- data/app/channels/collavre/comments_presence_channel.rb +54 -0
- data/app/channels/collavre/slide_view_channel.rb +11 -0
- data/app/channels/collavre/topics_channel.rb +12 -0
- data/app/components/collavre/avatar_component.html.erb +15 -0
- data/app/components/collavre/avatar_component.rb +59 -0
- data/app/components/collavre/inbox/badge_component.html.erb +6 -0
- data/app/components/collavre/inbox/badge_component.rb +18 -0
- data/app/components/collavre/plans_timeline_component.html.erb +14 -0
- data/app/components/collavre/plans_timeline_component.rb +56 -0
- data/app/components/collavre/popup_menu_component.html.erb +6 -0
- data/app/components/collavre/popup_menu_component.rb +30 -0
- data/app/components/collavre/progress_filter_component.html.erb +5 -0
- data/app/components/collavre/progress_filter_component.rb +10 -0
- data/app/components/collavre/user_mention_menu_component.html.erb +3 -0
- data/app/components/collavre/user_mention_menu_component.rb +8 -0
- data/app/controllers/collavre/application_controller.rb +15 -0
- data/app/controllers/collavre/attachments_controller.rb +44 -0
- data/app/controllers/collavre/calendar_events_controller.rb +15 -0
- data/app/controllers/collavre/comment_read_pointers_controller.rb +80 -0
- data/app/controllers/collavre/comments/activity_logs_controller.rb +26 -0
- data/app/controllers/collavre/comments/reactions_controller.rb +82 -0
- data/app/controllers/collavre/comments_controller.rb +464 -0
- data/app/controllers/collavre/contacts_controller.rb +10 -0
- data/app/controllers/collavre/creative_expanded_states_controller.rb +27 -0
- data/app/controllers/collavre/creative_imports_controller.rb +24 -0
- data/app/controllers/collavre/creative_plans_controller.rb +69 -0
- data/app/controllers/collavre/creative_shares_controller.rb +79 -0
- data/app/controllers/collavre/creatives_controller.rb +535 -0
- data/app/controllers/collavre/devices_controller.rb +19 -0
- data/app/controllers/collavre/email_verifications_controller.rb +16 -0
- data/app/controllers/collavre/emails_controller.rb +11 -0
- data/app/controllers/collavre/github_auth_controller.rb +25 -0
- data/app/controllers/collavre/google_auth_controller.rb +43 -0
- data/app/controllers/collavre/inbox_items_controller.rb +64 -0
- data/app/controllers/collavre/invites_controller.rb +27 -0
- data/app/controllers/collavre/notion_auth_controller.rb +25 -0
- data/app/controllers/collavre/passwords_controller.rb +37 -0
- data/app/controllers/collavre/plans_controller.rb +110 -0
- data/app/controllers/collavre/sessions_controller.rb +57 -0
- data/app/controllers/collavre/topics_controller.rb +58 -0
- data/app/controllers/collavre/user_themes_controller.rb +58 -0
- data/app/controllers/collavre/users_controller.rb +390 -0
- data/app/helpers/collavre/application_helper.rb +4 -0
- data/app/helpers/collavre/comments_helper.rb +9 -0
- data/app/helpers/collavre/creatives_helper.rb +343 -0
- data/app/helpers/collavre/navigation_helper.rb +163 -0
- data/app/helpers/collavre/user_themes_helper.rb +4 -0
- data/app/javascript/collavre.js +26 -0
- data/app/javascript/components/InlineLexicalEditor.jsx +889 -0
- data/app/javascript/components/LinkPopup.jsx +112 -0
- data/app/javascript/components/creative_tree_row.js +503 -0
- data/app/javascript/components/plugins/attachment_cleanup_plugin.jsx +95 -0
- data/app/javascript/components/plugins/image_upload_plugin.jsx +162 -0
- data/app/javascript/controllers/click_target_controller.js +13 -0
- data/app/javascript/controllers/comment_controller.js +162 -0
- data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +68 -0
- data/app/javascript/controllers/comments/form_controller.js +530 -0
- data/app/javascript/controllers/comments/list_controller.js +715 -0
- data/app/javascript/controllers/comments/mention_menu_controller.js +41 -0
- data/app/javascript/controllers/comments/popup_controller.js +385 -0
- data/app/javascript/controllers/comments/presence_controller.js +311 -0
- data/app/javascript/controllers/comments/topics_controller.js +338 -0
- data/app/javascript/controllers/common_popup_controller.js +55 -0
- data/app/javascript/controllers/creatives/drag_drop_controller.js +45 -0
- data/app/javascript/controllers/creatives/expansion_controller.js +222 -0
- data/app/javascript/controllers/creatives/import_controller.js +116 -0
- data/app/javascript/controllers/creatives/row_editor_controller.js +8 -0
- data/app/javascript/controllers/creatives/select_mode_controller.js +231 -0
- data/app/javascript/controllers/creatives/set_plan_modal_controller.js +107 -0
- data/app/javascript/controllers/creatives/tree_controller.js +218 -0
- data/app/javascript/controllers/index.js +79 -0
- data/app/javascript/controllers/link_creative_controller.js +91 -0
- data/app/javascript/controllers/popup_menu_controller.js +82 -0
- data/app/javascript/controllers/progress_filter_controller.js +35 -0
- data/app/javascript/controllers/reaction_picker_controller.js +107 -0
- data/app/javascript/controllers/share_invite_controller.js +15 -0
- data/app/javascript/controllers/share_user_search_controller.js +121 -0
- data/app/javascript/controllers/tabs_controller.js +43 -0
- data/app/javascript/creatives/drag_drop/dom.js +170 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +846 -0
- data/app/javascript/creatives/drag_drop/indicator.js +35 -0
- data/app/javascript/creatives/drag_drop/operations.js +116 -0
- data/app/javascript/creatives/drag_drop/state.js +31 -0
- data/app/javascript/creatives/tree_renderer.js +248 -0
- data/app/javascript/lib/api/__tests__/queue_manager.test.js +153 -0
- data/app/javascript/lib/api/creatives.js +79 -0
- data/app/javascript/lib/api/csrf_fetch.js +22 -0
- data/app/javascript/lib/api/drag_drop.js +31 -0
- data/app/javascript/lib/api/queue_manager.js +423 -0
- data/app/javascript/lib/apply_lexical_styles.js +15 -0
- data/app/javascript/lib/common_popup.js +195 -0
- data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +91 -0
- data/app/javascript/lib/lexical/__tests__/attachment_payload.test.js +194 -0
- data/app/javascript/lib/lexical/action_text_attachment_node.js +459 -0
- data/app/javascript/lib/lexical/attachment_node.jsx +170 -0
- data/app/javascript/lib/lexical/attachment_payload.js +293 -0
- data/app/javascript/lib/lexical/dom_attachment_utils.js +66 -0
- data/app/javascript/lib/lexical/image_node.jsx +159 -0
- data/app/javascript/lib/lexical/style_attributes.js +40 -0
- data/app/javascript/lib/responsive_images.js +54 -0
- data/app/javascript/lib/turbo_stream_actions.js +33 -0
- data/app/javascript/lib/utils/markdown.js +23 -0
- data/app/javascript/modules/creative_guide.js +53 -0
- data/app/javascript/modules/creative_row_editor.js +1841 -0
- data/app/javascript/modules/creative_row_swipe.js +43 -0
- data/app/javascript/modules/creatives.js +15 -0
- data/app/javascript/modules/export_to_markdown.js +34 -0
- data/app/javascript/modules/inbox_panel.js +226 -0
- data/app/javascript/modules/lexical_inline_editor.jsx +133 -0
- data/app/javascript/modules/mention_menu.js +77 -0
- data/app/javascript/modules/plans_menu.js +39 -0
- data/app/javascript/modules/plans_timeline.js +397 -0
- data/app/javascript/modules/share_modal.js +73 -0
- data/app/javascript/modules/share_user_popup.js +77 -0
- data/app/javascript/modules/slide_view.js +163 -0
- data/app/javascript/services/cable.js +32 -0
- data/app/javascript/slide_view.js +2 -0
- data/app/javascript/utils/caret_position.js +42 -0
- data/app/javascript/utils/clipboard.js +40 -0
- data/app/jobs/collavre/ai_agent_job.rb +27 -0
- data/app/jobs/collavre/inbox_summary_job.rb +24 -0
- data/app/jobs/collavre/notion_export_job.rb +30 -0
- data/app/jobs/collavre/notion_sync_job.rb +48 -0
- data/app/jobs/collavre/permission_cache_cleanup_job.rb +36 -0
- data/app/jobs/collavre/permission_cache_job.rb +71 -0
- data/app/jobs/collavre/push_notification_job.rb +86 -0
- data/app/mailers/collavre/application_mailer.rb +17 -0
- data/app/mailers/collavre/creative_mailer.rb +9 -0
- data/app/mailers/collavre/email_verification_mailer.rb +20 -0
- data/app/mailers/collavre/inbox_mailer.rb +19 -0
- data/app/mailers/collavre/invitation_mailer.rb +16 -0
- data/app/mailers/collavre/passwords_mailer.rb +10 -0
- data/app/models/collavre/activity_log.rb +13 -0
- data/app/models/collavre/application_record.rb +5 -0
- data/app/models/collavre/calendar_event.rb +20 -0
- data/app/models/collavre/comment.rb +307 -0
- data/app/models/collavre/comment_presence_store.rb +30 -0
- data/app/models/collavre/comment_reaction.rb +11 -0
- data/app/models/collavre/comment_read_pointer.rb +26 -0
- data/app/models/collavre/contact.rb +23 -0
- data/app/models/collavre/creative.rb +413 -0
- data/app/models/collavre/creative_expanded_state.rb +11 -0
- data/app/models/collavre/creative_share.rb +122 -0
- data/app/models/collavre/creative_shares_cache.rb +18 -0
- data/app/models/collavre/current.rb +14 -0
- data/app/models/collavre/device.rb +13 -0
- data/app/models/collavre/email.rb +14 -0
- data/app/models/collavre/github_account.rb +10 -0
- data/app/models/collavre/github_repository_link.rb +19 -0
- data/app/models/collavre/inbox_item.rb +95 -0
- data/app/models/collavre/invitation.rb +22 -0
- data/app/models/collavre/label.rb +47 -0
- data/app/models/collavre/mcp_tool.rb +30 -0
- data/app/models/collavre/notion_account.rb +17 -0
- data/app/models/collavre/notion_block_link.rb +10 -0
- data/app/models/collavre/notion_page_link.rb +19 -0
- data/app/models/collavre/plan.rb +20 -0
- data/app/models/collavre/session.rb +24 -0
- data/app/models/collavre/system_setting.rb +144 -0
- data/app/models/collavre/tag.rb +10 -0
- data/app/models/collavre/task.rb +10 -0
- data/app/models/collavre/task_action.rb +10 -0
- data/app/models/collavre/topic.rb +12 -0
- data/app/models/collavre/user.rb +174 -0
- data/app/models/collavre/user_theme.rb +10 -0
- data/app/models/collavre/variation.rb +5 -0
- data/app/models/collavre/webauthn_credential.rb +11 -0
- data/app/services/collavre/ai_agent_service.rb +193 -0
- data/app/services/collavre/ai_client.rb +183 -0
- data/app/services/collavre/ai_system_prompt_renderer.rb +38 -0
- data/app/services/collavre/auto_theme_generator.rb +198 -0
- data/app/services/collavre/comment_link_formatter.rb +60 -0
- data/app/services/collavre/comments/action_executor.rb +262 -0
- data/app/services/collavre/comments/action_validator.rb +58 -0
- data/app/services/collavre/comments/calendar_command.rb +97 -0
- data/app/services/collavre/comments/command_processor.rb +37 -0
- data/app/services/collavre/comments/mcp_command.rb +109 -0
- data/app/services/collavre/comments/mcp_command_builder.rb +32 -0
- data/app/services/collavre/creatives/filter_pipeline.rb +196 -0
- data/app/services/collavre/creatives/filters/assignee_filter.rb +30 -0
- data/app/services/collavre/creatives/filters/base_filter.rb +24 -0
- data/app/services/collavre/creatives/filters/comment_filter.rb +21 -0
- data/app/services/collavre/creatives/filters/date_filter.rb +58 -0
- data/app/services/collavre/creatives/filters/progress_filter.rb +25 -0
- data/app/services/collavre/creatives/filters/search_filter.rb +28 -0
- data/app/services/collavre/creatives/filters/tag_filter.rb +16 -0
- data/app/services/collavre/creatives/importer.rb +47 -0
- data/app/services/collavre/creatives/index_query.rb +191 -0
- data/app/services/collavre/creatives/path_exporter.rb +131 -0
- data/app/services/collavre/creatives/permission_cache_builder.rb +194 -0
- data/app/services/collavre/creatives/permission_checker.rb +42 -0
- data/app/services/collavre/creatives/plan_tagger.rb +53 -0
- data/app/services/collavre/creatives/progress_service.rb +89 -0
- data/app/services/collavre/creatives/reorderer.rb +187 -0
- data/app/services/collavre/creatives/tree_builder.rb +231 -0
- data/app/services/collavre/creatives/tree_formatter.rb +36 -0
- data/app/services/collavre/gemini_parent_recommender.rb +77 -0
- data/app/services/collavre/github/client.rb +112 -0
- data/app/services/collavre/github/pull_request_analyzer.rb +280 -0
- data/app/services/collavre/github/pull_request_processor.rb +181 -0
- data/app/services/collavre/github/webhook_provisioner.rb +130 -0
- data/app/services/collavre/google_calendar_service.rb +149 -0
- data/app/services/collavre/link_preview_fetcher.rb +230 -0
- data/app/services/collavre/markdown_importer.rb +202 -0
- data/app/services/collavre/mcp_service.rb +217 -0
- data/app/services/collavre/notion_client.rb +231 -0
- data/app/services/collavre/notion_creative_exporter.rb +296 -0
- data/app/services/collavre/notion_service.rb +249 -0
- data/app/services/collavre/ppt_importer.rb +76 -0
- data/app/services/collavre/ruby_llm_interaction_logger.rb +34 -0
- data/app/services/collavre/system_events/context_builder.rb +41 -0
- data/app/services/collavre/system_events/dispatcher.rb +19 -0
- data/app/services/collavre/system_events/router.rb +72 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +138 -0
- data/app/views/admin/shared/_tabs.html.erb +4 -0
- data/app/views/collavre/comments/_activity_log_details.html.erb +23 -0
- data/app/views/collavre/comments/_comment.html.erb +147 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +46 -0
- data/app/views/collavre/comments/_list.html.erb +10 -0
- data/app/views/collavre/comments/_presence_avatars.html.erb +8 -0
- data/app/views/collavre/comments/_reaction_picker.html.erb +15 -0
- data/app/views/collavre/comments/_read_receipts.html.erb +19 -0
- data/app/views/collavre/creatives/_add_button.html.erb +20 -0
- data/app/views/collavre/creatives/_delete_button.html.erb +12 -0
- data/app/views/collavre/creatives/_github_integration_modal.html.erb +77 -0
- data/app/views/collavre/creatives/_import_upload_zone.html.erb +10 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +89 -0
- data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +24 -0
- data/app/views/collavre/creatives/_notion_integration_modal.html.erb +90 -0
- data/app/views/collavre/creatives/_set_plan_modal.html.erb +25 -0
- data/app/views/collavre/creatives/_share_button.html.erb +55 -0
- data/app/views/collavre/creatives/edit.html.erb +4 -0
- data/app/views/collavre/creatives/index.html.erb +192 -0
- data/app/views/collavre/creatives/new.html.erb +4 -0
- data/app/views/collavre/creatives/show.html.erb +29 -0
- data/app/views/collavre/creatives/slide_view.html.erb +20 -0
- data/app/views/collavre/email_verification_mailer/verify.html.erb +4 -0
- data/app/views/collavre/email_verification_mailer/verify.text.erb +2 -0
- data/app/views/collavre/emails/index.html.erb +19 -0
- data/app/views/collavre/emails/show.html.erb +5 -0
- data/app/views/collavre/inbox_items/_item.html.erb +18 -0
- data/app/views/collavre/inbox_items/_items.html.erb +3 -0
- data/app/views/collavre/inbox_items/_list.html.erb +7 -0
- data/app/views/collavre/inbox_items/index.html.erb +1 -0
- data/app/views/collavre/inbox_mailer/daily_summary.html.erb +11 -0
- data/app/views/collavre/inbox_mailer/daily_summary.text.erb +8 -0
- data/app/views/collavre/invitation_mailer/invite.html.erb +5 -0
- data/app/views/collavre/invitation_mailer/invite.text.erb +3 -0
- data/app/views/collavre/invites/show.html.erb +8 -0
- data/app/views/collavre/passwords/edit.html.erb +12 -0
- data/app/views/collavre/passwords/new.html.erb +17 -0
- data/app/views/collavre/passwords_mailer/reset.html.erb +4 -0
- data/app/views/collavre/passwords_mailer/reset.text.erb +2 -0
- data/app/views/collavre/sessions/new.html.erb +27 -0
- data/app/views/collavre/sessions/providers/_google.html.erb +7 -0
- data/app/views/collavre/sessions/providers/_passkey.html.erb +3 -0
- data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -0
- data/app/views/collavre/shared/_navigation.html.erb +37 -0
- data/app/views/collavre/shared/navigation/_help_button.html.erb +1 -0
- data/app/views/collavre/shared/navigation/_inbox_button.html.erb +5 -0
- data/app/views/collavre/shared/navigation/_mobile_inbox_button.html.erb +3 -0
- data/app/views/collavre/shared/navigation/_mobile_plans_button.html.erb +1 -0
- data/app/views/collavre/shared/navigation/_panels.html.erb +18 -0
- data/app/views/collavre/shared/navigation/_plans_button.html.erb +3 -0
- data/app/views/collavre/shared/navigation/_search_form.html.erb +6 -0
- data/app/views/collavre/user_themes/index.html.erb +75 -0
- data/app/views/collavre/users/_app_header.html.erb +5 -0
- data/app/views/collavre/users/_contact_management.html.erb +77 -0
- data/app/views/collavre/users/_id_pwd_fields.html.erb +27 -0
- data/app/views/collavre/users/edit_ai.html.erb +79 -0
- data/app/views/collavre/users/edit_password.html.erb +27 -0
- data/app/views/collavre/users/index.html.erb +80 -0
- data/app/views/collavre/users/new.html.erb +33 -0
- data/app/views/collavre/users/new_ai.html.erb +87 -0
- data/app/views/collavre/users/passkeys.html.erb +43 -0
- data/app/views/collavre/users/show.html.erb +143 -0
- data/app/views/inbox/badge_component/_count.html.erb +2 -0
- data/app/views/layouts/collavre/slide.html.erb +29 -0
- data/config/locales/comments.en.yml +114 -0
- data/config/locales/comments.ko.yml +110 -0
- data/config/locales/contacts.en.yml +16 -0
- data/config/locales/contacts.ko.yml +16 -0
- data/config/locales/creatives.en.yml +183 -0
- data/config/locales/creatives.ko.yml +164 -0
- data/config/locales/invites.en.yml +19 -0
- data/config/locales/invites.ko.yml +17 -0
- data/config/locales/notifications.en.yml +8 -0
- data/config/locales/notifications.ko.yml +8 -0
- data/config/locales/plans.en.yml +12 -0
- data/config/locales/plans.ko.yml +19 -0
- data/config/locales/themes.en.yml +29 -0
- data/config/locales/themes.ko.yml +27 -0
- data/config/locales/users.en.yml +151 -0
- data/config/locales/users.ko.yml +146 -0
- data/config/routes.rb +92 -0
- data/db/migrate/20241201000000_create_notion_integrations.rb +29 -0
- data/db/migrate/20250128110017_create_creatives.rb +11 -0
- data/db/migrate/20250128120122_create_users.rb +11 -0
- data/db/migrate/20250128120123_create_sessions.rb +11 -0
- data/db/migrate/20250128123633_create_subscribers.rb +10 -0
- data/db/migrate/20250312000000_create_notion_block_links.rb +16 -0
- data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +5 -0
- data/db/migrate/20250522115048_remove_name_from_creatives.rb +5 -0
- data/db/migrate/20250522190651_add_parent_id_to_creatives.rb +5 -0
- data/db/migrate/20250523133100_rename_inventory_count_to_progress.rb +13 -0
- data/db/migrate/20250523133101_add_sequence_to_creatives.rb +5 -0
- data/db/migrate/20250525205100_add_user_id_to_creatives.rb +10 -0
- data/db/migrate/20250527014217_create_creative_shares.rb +12 -0
- data/db/migrate/20250528142349_add_origin_id_to_creatives.rb +5 -0
- data/db/migrate/20250530060200_create_tags.rb +9 -0
- data/db/migrate/20250531105150_add_value_to_tags.rb +5 -0
- data/db/migrate/20250531140142_create_labels.rb +12 -0
- data/db/migrate/20250531140145_change_tag_to_label_reference.rb +6 -0
- data/db/migrate/20250601000000_create_comments.rb +10 -0
- data/db/migrate/20250601061830_drop_plans_and_variations.rb +6 -0
- data/db/migrate/20250604122600_add_owner_to_labels.rb +5 -0
- data/db/migrate/20250606000000_rename_email_address_to_email_in_users.rb +7 -0
- data/db/migrate/20250606150329_create_creative_hierarchies.rb +16 -0
- data/db/migrate/20250610142000_create_creative_expanded_states.rb +11 -0
- data/db/migrate/20250611030138_create_invitations.rb +15 -0
- data/db/migrate/20250611105524_create_inbox_items.rb +14 -0
- data/db/migrate/20250612150000_update_permissions.rb +43 -0
- data/db/migrate/20250612232913_add_email_verified_at_to_users.rb +5 -0
- data/db/migrate/20250616065905_add_avatar_to_users.rb +5 -0
- data/db/migrate/20250617092111_add_display_level_to_users.rb +5 -0
- data/db/migrate/20250620004558_create_emails.rb +13 -0
- data/db/migrate/20250621000000_create_comment_read_pointers.rb +11 -0
- data/db/migrate/20250622000000_add_completion_mark_to_users.rb +5 -0
- data/db/migrate/20250623000000_add_theme_to_users.rb +5 -0
- data/db/migrate/20250624000000_add_name_to_users.rb +18 -0
- data/db/migrate/20250714190000_create_devices.rb +17 -0
- data/db/migrate/20250715120000_add_notifications_enabled_to_users.rb +5 -0
- data/db/migrate/20250823000000_add_no_access_permission.rb +25 -0
- data/db/migrate/20250826000000_add_calendar_id_to_users.rb +5 -0
- data/db/migrate/20250827000000_add_google_oauth_tokens_to_users.rb +8 -0
- data/db/migrate/20250827061238_add_timezone_to_users.rb +5 -0
- data/db/migrate/20250828000000_create_calendar_events.rb +16 -0
- data/db/migrate/20250828060000_add_creative_to_calendar_events.rb +5 -0
- data/db/migrate/20250830141052_add_locale_to_users.rb +5 -0
- data/db/migrate/20250830141101_add_message_key_to_inbox_items.rb +6 -0
- data/db/migrate/20250902025423_remove_description_and_featured_image_from_creatives.rb +6 -0
- data/db/migrate/20250910000000_add_private_to_comments.rb +5 -0
- data/db/migrate/20250910105640_migrate_write_permissions_to_admin.rb +17 -0
- data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +27 -0
- data/db/migrate/20250923002959_deduplicate_device_fcm_tokens.rb +25 -0
- data/db/migrate/20250924000000_add_system_admin_to_users.rb +6 -0
- data/db/migrate/20250925000000_create_github_integrations.rb +26 -0
- data/db/migrate/20250927000000_add_webhook_secret_to_github_repository_links.rb +29 -0
- data/db/migrate/20250928000000_add_action_and_approver_to_comments.rb +6 -0
- data/db/migrate/20250928010000_add_action_execution_tracking_to_comments.rb +6 -0
- data/db/migrate/20250928105957_add_github_gemini_prompt_to_creatives.rb +5 -0
- data/db/migrate/20250929000000_add_comment_and_creative_refs_to_inbox_items.rb +6 -0
- data/db/migrate/20251001000001_create_contacts.rb +71 -0
- data/db/migrate/20251002000000_add_shared_by_to_creative_shares.rb +14 -0
- data/db/migrate/20251124120902_add_ai_fields_to_users.rb +7 -0
- data/db/migrate/20251124122218_add_created_by_id_to_users.rb +5 -0
- data/db/migrate/20251124124521_add_llm_api_key_to_users.rb +5 -0
- data/db/migrate/20251124130000_add_searchable_to_users.rb +6 -0
- data/db/migrate/20251125072705_migrate_linked_creative_children_to_origin.rb +28 -0
- data/db/migrate/20251126040752_add_description_to_creatives.rb +75 -0
- data/db/migrate/20251127000000_move_comments_from_linked_creatives_to_origins.rb +18 -0
- data/db/migrate/20251201073823_add_tools_to_users.rb +5 -0
- data/db/migrate/20251202062715_create_mcp_tools.rb +16 -0
- data/db/migrate/20251204125754_create_tasks_and_task_actions.rb +23 -0
- data/db/migrate/20251204125756_add_routing_expression_to_users.rb +5 -0
- data/db/migrate/20251204161133_set_default_routing_expression_for_ai_agents.rb +13 -0
- data/db/migrate/20251211033025_nullify_self_referencing_origins.rb +9 -0
- data/db/migrate/20251211080040_create_topics_and_add_topic_to_comments.rb +15 -0
- data/db/migrate/20251215143500_create_activity_logs.rb +16 -0
- data/db/migrate/20251222125727_create_webauthn_credentials.rb +14 -0
- data/db/migrate/20251222125839_add_webauthn_id_to_users.rb +5 -0
- data/db/migrate/20251223022315_create_comment_reactions.rb +13 -0
- data/db/migrate/20251223072625_create_user_themes.rb +11 -0
- data/db/migrate/20251230074456_add_creative_id_to_labels.rb +5 -0
- data/db/migrate/20251230113607_refactor_labels.rb +38 -0
- data/db/migrate/20251231010012_backfill_tags_for_labels.rb +15 -0
- data/db/migrate/20251231013234_drop_subscribers.rb +9 -0
- data/db/migrate/20260106090544_allow_null_user_id_in_creative_shares.rb +6 -0
- data/db/migrate/20260106160643_create_system_settings.rb +11 -0
- data/db/migrate/20260116000000_create_creative_shares_cache.rb +15 -0
- data/db/migrate/20260116000001_populate_creative_shares_cache.rb +23 -0
- data/db/migrate/20260119022933_make_source_share_id_nullable_in_creative_shares_caches.rb +7 -0
- data/db/migrate/20260119023446_populate_owner_cache_entries.rb +25 -0
- data/db/migrate/20260119100000_add_account_lockout_to_users.rb +6 -0
- data/db/migrate/20260119110000_add_last_active_at_to_sessions.rb +6 -0
- data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +60 -0
- data/db/migrate/20260120162259_remove_fk_from_creative_shares_caches.rb +7 -0
- data/db/migrate/20260120163856_remove_timestamps_from_creative_shares_caches.rb +6 -0
- data/lib/collavre/configuration.rb +14 -0
- data/lib/collavre/engine.rb +77 -0
- data/lib/collavre/user_extensions.rb +29 -0
- data/lib/collavre/version.rb +3 -0
- data/lib/collavre.rb +26 -0
- data/lib/generators/collavre/install/install_generator.rb +105 -0
- data/lib/generators/collavre/install/templates/build.cjs.tt +100 -0
- data/lib/tasks/collavre_assets.rake +15 -0
- metadata +591 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import csrfFetch from './csrf_fetch';
|
|
2
|
+
|
|
3
|
+
export function sendNewOrder({ draggedId, draggedIds, targetId, direction }) {
|
|
4
|
+
const payload = { target_id: targetId, direction };
|
|
5
|
+
if (Array.isArray(draggedIds) && draggedIds.length > 0) {
|
|
6
|
+
payload.dragged_ids = draggedIds;
|
|
7
|
+
} else {
|
|
8
|
+
payload.dragged_id = draggedId;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return csrfFetch('/creatives/reorder', {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
},
|
|
16
|
+
body: JSON.stringify(payload),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sendLinkedCreative({ draggedId, targetId, direction }) {
|
|
21
|
+
return csrfFetch('/creatives/link_drop', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify({ dragged_id: draggedId, target_id: targetId, direction }),
|
|
27
|
+
}).then((response) => {
|
|
28
|
+
if (!response.ok) throw new Error('Failed to create linked creative');
|
|
29
|
+
return response.json();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import csrfFetch from './csrf_fetch'
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'api_queue'
|
|
4
|
+
const MAX_RETRIES = 3
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* API Queue Manager
|
|
8
|
+
* Manages asynchronous API requests with localStorage persistence,
|
|
9
|
+
* sequential processing, retry logic, and deduplication.
|
|
10
|
+
*/
|
|
11
|
+
class ApiQueueManager {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.queue = []
|
|
14
|
+
this.failedItems = []
|
|
15
|
+
this.processing = false
|
|
16
|
+
this.storageKey = 'api_queue_guest' // Default fallback
|
|
17
|
+
this.setupNetworkListeners()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize the queue for a specific user
|
|
22
|
+
* @param {string|number} userId - Current user ID
|
|
23
|
+
*/
|
|
24
|
+
initialize(userId) {
|
|
25
|
+
this.userId = userId
|
|
26
|
+
this.storageKey = userId ? `api_queue_${userId}` : 'api_queue_guest'
|
|
27
|
+
this.loadFromLocalStorage()
|
|
28
|
+
this.loadFailedFromLocalStorage()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load pending requests from localStorage
|
|
33
|
+
*/
|
|
34
|
+
loadFromLocalStorage() {
|
|
35
|
+
try {
|
|
36
|
+
const stored = localStorage.getItem(this.storageKey)
|
|
37
|
+
if (stored) {
|
|
38
|
+
this.queue = JSON.parse(stored)
|
|
39
|
+
// Note: We don't auto-start processing here anymore.
|
|
40
|
+
// The consumer (creative_row_editor.js) must call start() explicitly
|
|
41
|
+
// after registering event listeners to avoid missing events.
|
|
42
|
+
} else {
|
|
43
|
+
this.queue = []
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Failed to load API queue from localStorage:', error)
|
|
47
|
+
this.queue = []
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load failed requests from localStorage so we don't lose them after reload
|
|
53
|
+
*/
|
|
54
|
+
loadFailedFromLocalStorage() {
|
|
55
|
+
try {
|
|
56
|
+
const stored = localStorage.getItem(`${this.storageKey}_failed`)
|
|
57
|
+
if (stored) {
|
|
58
|
+
this.failedItems = JSON.parse(stored)
|
|
59
|
+
} else {
|
|
60
|
+
this.failedItems = []
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Failed to load failed API queue from localStorage:', error)
|
|
64
|
+
this.failedItems = []
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Start processing the queue
|
|
70
|
+
* Should be called after event listeners are registered
|
|
71
|
+
*/
|
|
72
|
+
start() {
|
|
73
|
+
if (this.queue.length > 0) {
|
|
74
|
+
this.processQueue()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Save queue to localStorage
|
|
80
|
+
* Items with onSuccess callbacks are excluded because functions cannot be serialized
|
|
81
|
+
* Items with deletedAttachmentIds are included because they're serializable data
|
|
82
|
+
*/
|
|
83
|
+
saveToLocalStorage() {
|
|
84
|
+
try {
|
|
85
|
+
// Filter out onSuccess callbacks (non-serializable)
|
|
86
|
+
// but keep the items themselves
|
|
87
|
+
const serializableQueue = this.queue.map(item => {
|
|
88
|
+
// eslint-disable-next-line no-unused-vars
|
|
89
|
+
const { onSuccess, ...serializableItem } = item
|
|
90
|
+
return serializableItem
|
|
91
|
+
})
|
|
92
|
+
localStorage.setItem(this.storageKey, JSON.stringify(serializableQueue))
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Failed to save API queue to localStorage:', error)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Persist failed items separately so they can be surfaced to the user later
|
|
100
|
+
*/
|
|
101
|
+
saveFailedToLocalStorage() {
|
|
102
|
+
try {
|
|
103
|
+
localStorage.setItem(`${this.storageKey}_failed`, JSON.stringify(this.failedItems))
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Failed to save failed API queue to localStorage:', error)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Setup network status listeners
|
|
111
|
+
*/
|
|
112
|
+
setupNetworkListeners() {
|
|
113
|
+
window.addEventListener('online', () => {
|
|
114
|
+
console.log('Network online - processing queue')
|
|
115
|
+
this.processQueue()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
window.addEventListener('offline', () => {
|
|
119
|
+
console.log('Network offline - queue will resume when online')
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Add a request to the queue
|
|
125
|
+
* @param {Object} request - Request configuration
|
|
126
|
+
* @param {string} request.path - API path
|
|
127
|
+
* @param {string} request.method - HTTP method (GET, POST, PATCH, DELETE)
|
|
128
|
+
* @param {Object} request.params - URL parameters
|
|
129
|
+
* @param {Object} request.body - Request body
|
|
130
|
+
* @param {string} request.dedupeKey - Optional key for deduplication
|
|
131
|
+
* @param {Function} request.onSuccess - Optional callback to run after successful request
|
|
132
|
+
* @returns {string} Request ID
|
|
133
|
+
*/
|
|
134
|
+
enqueue(request) {
|
|
135
|
+
// Find and merge callbacks and attachment IDs from existing requests with the same dedupeKey
|
|
136
|
+
let existingCallbacks = []
|
|
137
|
+
let existingAttachmentIds = []
|
|
138
|
+
if (request.dedupeKey) {
|
|
139
|
+
// CRITICAL: Skip the first item if processing is active
|
|
140
|
+
// The first item might be currently executing in processQueue
|
|
141
|
+
// Removing it would cause shift() to remove the wrong item
|
|
142
|
+
const startIndex = this.processing ? 1 : 0
|
|
143
|
+
const existingItems = this.queue.slice(startIndex).filter(item => item.dedupeKey === request.dedupeKey)
|
|
144
|
+
|
|
145
|
+
existingItems.forEach(item => {
|
|
146
|
+
if (typeof item.onSuccess === 'function') {
|
|
147
|
+
existingCallbacks.push(item.onSuccess)
|
|
148
|
+
}
|
|
149
|
+
if (item.deletedAttachmentIds && item.deletedAttachmentIds.length > 0) {
|
|
150
|
+
existingAttachmentIds.push(...item.deletedAttachmentIds)
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Remove existing requests with the same dedupeKey
|
|
155
|
+
// CRITICAL: Keep the first item if processing is active
|
|
156
|
+
if (this.processing) {
|
|
157
|
+
const firstItem = this.queue[0]
|
|
158
|
+
this.queue = [firstItem, ...this.queue.slice(1).filter(item => item.dedupeKey !== request.dedupeKey)]
|
|
159
|
+
} else {
|
|
160
|
+
this.queue = this.queue.filter(item => item.dedupeKey !== request.dedupeKey)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Merge attachment IDs
|
|
165
|
+
let mergedAttachmentIds = null
|
|
166
|
+
if (request.deletedAttachmentIds && request.deletedAttachmentIds.length > 0) {
|
|
167
|
+
existingAttachmentIds.push(...request.deletedAttachmentIds)
|
|
168
|
+
}
|
|
169
|
+
if (existingAttachmentIds.length > 0) {
|
|
170
|
+
// Remove duplicates
|
|
171
|
+
mergedAttachmentIds = [...new Set(existingAttachmentIds)]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Merge new callback with existing callbacks
|
|
175
|
+
let mergedCallback = null
|
|
176
|
+
if (existingCallbacks.length > 0 || request.onSuccess) {
|
|
177
|
+
mergedCallback = () => {
|
|
178
|
+
// Run all existing callbacks first
|
|
179
|
+
existingCallbacks.forEach(cb => {
|
|
180
|
+
try {
|
|
181
|
+
cb()
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error('Merged callback failed:', error)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
// Then run the new callback
|
|
187
|
+
if (typeof request.onSuccess === 'function') {
|
|
188
|
+
try {
|
|
189
|
+
request.onSuccess()
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('New callback failed:', error)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const queueItem = {
|
|
198
|
+
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
199
|
+
path: request.path,
|
|
200
|
+
method: request.method || 'GET',
|
|
201
|
+
params: request.params || null,
|
|
202
|
+
body: request.body || null,
|
|
203
|
+
dedupeKey: request.dedupeKey || null,
|
|
204
|
+
deletedAttachmentIds: mergedAttachmentIds,
|
|
205
|
+
onSuccess: mergedCallback,
|
|
206
|
+
timestamp: Date.now(),
|
|
207
|
+
retries: 0
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.queue.push(queueItem)
|
|
211
|
+
this.saveToLocalStorage()
|
|
212
|
+
|
|
213
|
+
// Start processing if not already processing
|
|
214
|
+
this.processQueue()
|
|
215
|
+
|
|
216
|
+
return queueItem.id
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Process all queued requests sequentially
|
|
221
|
+
*/
|
|
222
|
+
async processQueue() {
|
|
223
|
+
if (this.processing || this.queue.length === 0) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.processing = true
|
|
228
|
+
|
|
229
|
+
while (this.queue.length > 0) {
|
|
230
|
+
const item = this.queue[0]
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await this.executeRequest(item)
|
|
234
|
+
// Success - handle cleanup actions
|
|
235
|
+
|
|
236
|
+
// Dispatch event for attachment cleanup if needed
|
|
237
|
+
if (item.deletedAttachmentIds && item.deletedAttachmentIds.length > 0) {
|
|
238
|
+
window.dispatchEvent(new CustomEvent('api-queue-attachments-deleted', {
|
|
239
|
+
detail: { attachmentIds: item.deletedAttachmentIds }
|
|
240
|
+
}))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Call onSuccess callback if provided (for non-serializable actions)
|
|
244
|
+
if (typeof item.onSuccess === 'function') {
|
|
245
|
+
try {
|
|
246
|
+
item.onSuccess()
|
|
247
|
+
} catch (callbackError) {
|
|
248
|
+
console.error('onSuccess callback failed:', callbackError)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Remove from queue
|
|
253
|
+
this.queue.shift()
|
|
254
|
+
this.saveToLocalStorage()
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error('API request failed:', error, item)
|
|
257
|
+
|
|
258
|
+
// Retry logic
|
|
259
|
+
if (item.retries < MAX_RETRIES) {
|
|
260
|
+
item.retries++
|
|
261
|
+
// Move to end of queue for retry
|
|
262
|
+
this.queue.shift()
|
|
263
|
+
this.queue.push(item)
|
|
264
|
+
this.saveToLocalStorage()
|
|
265
|
+
} else {
|
|
266
|
+
// Max retries exceeded - move to failedItems for visibility
|
|
267
|
+
console.error('Max retries exceeded, moving request to failed items:', item)
|
|
268
|
+
const failedItem = { ...item, failedAt: Date.now(), lastError: error?.message }
|
|
269
|
+
this.failedItems.push(failedItem)
|
|
270
|
+
this.saveFailedToLocalStorage()
|
|
271
|
+
this.queue.shift()
|
|
272
|
+
this.saveToLocalStorage()
|
|
273
|
+
this.handleFailedRequest(failedItem, error)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// If network error, stop processing and wait for online event
|
|
277
|
+
if (!navigator.onLine) {
|
|
278
|
+
console.log('Network offline - pausing queue processing')
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.processing = false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Execute a single API request
|
|
289
|
+
* @param {Object} item - Queue item
|
|
290
|
+
* @returns {Promise<Response>}
|
|
291
|
+
*/
|
|
292
|
+
async executeRequest(item) {
|
|
293
|
+
let url = item.path
|
|
294
|
+
|
|
295
|
+
// Add query parameters if present
|
|
296
|
+
if (item.params) {
|
|
297
|
+
const params = new URLSearchParams()
|
|
298
|
+
Object.keys(item.params).forEach(key => {
|
|
299
|
+
params.append(key, item.params[key])
|
|
300
|
+
})
|
|
301
|
+
const queryString = params.toString()
|
|
302
|
+
if (queryString) {
|
|
303
|
+
url = `${url}?${queryString}`
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const options = {
|
|
308
|
+
method: item.method,
|
|
309
|
+
headers: {
|
|
310
|
+
'Accept': 'application/json'
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Add body for POST/PATCH/PUT requests
|
|
315
|
+
if (item.body && ['POST', 'PATCH', 'PUT'].includes(item.method)) {
|
|
316
|
+
// If body is FormData, use it (or clone it if needed, but direct use is usually fine)
|
|
317
|
+
// We can iterate entries to be safe and consistent with previous fix intent
|
|
318
|
+
if (item.body instanceof FormData) {
|
|
319
|
+
const formData = new FormData()
|
|
320
|
+
// FormData.entries() iterator
|
|
321
|
+
for (const [key, value] of item.body.entries()) {
|
|
322
|
+
formData.append(key, value)
|
|
323
|
+
}
|
|
324
|
+
options.body = formData
|
|
325
|
+
}
|
|
326
|
+
// If body is a plain object, convert to FormData
|
|
327
|
+
else if (item.body && typeof item.body === 'object') {
|
|
328
|
+
const formData = new FormData()
|
|
329
|
+
Object.keys(item.body).forEach(key => {
|
|
330
|
+
const value = item.body[key]
|
|
331
|
+
if (value !== null && value !== undefined) {
|
|
332
|
+
formData.append(key, value)
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
options.body = formData
|
|
336
|
+
} else {
|
|
337
|
+
options.body = item.body
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const response = await csrfFetch(url, options)
|
|
342
|
+
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return response
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Handle permanently failed requests
|
|
352
|
+
* @param {Object} item - Failed queue item
|
|
353
|
+
* @param {Error} error - Error object
|
|
354
|
+
*/
|
|
355
|
+
handleFailedRequest(item, error) {
|
|
356
|
+
// Log to console or send to error tracking service
|
|
357
|
+
console.error('Request permanently failed:', {
|
|
358
|
+
item,
|
|
359
|
+
error: error.message,
|
|
360
|
+
timestamp: new Date().toISOString()
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
// Could dispatch a custom event for UI notification
|
|
364
|
+
window.dispatchEvent(new CustomEvent('api-queue-request-failed', {
|
|
365
|
+
detail: { item, error }
|
|
366
|
+
}))
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Clear all queued requests
|
|
371
|
+
*/
|
|
372
|
+
clear() {
|
|
373
|
+
this.queue = []
|
|
374
|
+
this.failedItems = []
|
|
375
|
+
this.saveToLocalStorage()
|
|
376
|
+
this.saveFailedToLocalStorage()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Remove items with a specific dedupe key
|
|
381
|
+
* @param {string} dedupeKey - Key to remove
|
|
382
|
+
*/
|
|
383
|
+
removeByDedupeKey(dedupeKey) {
|
|
384
|
+
if (!dedupeKey) return
|
|
385
|
+
|
|
386
|
+
// Filter out items with the matching key
|
|
387
|
+
// CRITICAL: If processing is active, we must NOT remove the first item
|
|
388
|
+
// as it is currently being executed. Removing it would cause state inconsistency.
|
|
389
|
+
if (this.processing && this.queue.length > 0) {
|
|
390
|
+
const firstItem = this.queue[0]
|
|
391
|
+
// If the currently processing item matches, we can't stop it,
|
|
392
|
+
// but we can remove subsequent queued items.
|
|
393
|
+
const rest = this.queue.slice(1).filter(item => item.dedupeKey !== dedupeKey)
|
|
394
|
+
this.queue = [firstItem, ...rest]
|
|
395
|
+
} else {
|
|
396
|
+
this.queue = this.queue.filter(item => item.dedupeKey !== dedupeKey)
|
|
397
|
+
}
|
|
398
|
+
this.saveToLocalStorage()
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get current queue status
|
|
403
|
+
* @returns {Object} Queue status
|
|
404
|
+
*/
|
|
405
|
+
getStatus() {
|
|
406
|
+
return {
|
|
407
|
+
queueLength: this.queue.length,
|
|
408
|
+
processing: this.processing,
|
|
409
|
+
failedLength: this.failedItems.length,
|
|
410
|
+
items: this.queue.map(item => ({
|
|
411
|
+
id: item.id,
|
|
412
|
+
path: item.path,
|
|
413
|
+
method: item.method,
|
|
414
|
+
retries: item.retries,
|
|
415
|
+
timestamp: item.timestamp
|
|
416
|
+
}))
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Export singleton instance
|
|
422
|
+
export const apiQueue = new ApiQueueManager()
|
|
423
|
+
export default apiQueue
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {syncLexicalStyleAttributes} from "./lexical/style_attributes"
|
|
2
|
+
|
|
3
|
+
export function applyLexicalStyles(root = document) {
|
|
4
|
+
syncLexicalStyleAttributes(root)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const handleLoad = (event) => {
|
|
8
|
+
const target = event?.target instanceof Element ? event.target : document
|
|
9
|
+
applyLexicalStyles(target)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
document.addEventListener("turbo:load", handleLoad)
|
|
13
|
+
document.addEventListener("turbo:frame-load", handleLoad)
|
|
14
|
+
|
|
15
|
+
applyLexicalStyles(document)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export default class CommonPopup {
|
|
2
|
+
constructor(element, { listElement, onSelect, renderItem, onClose, closeOnOutsideClick = true } = {}) {
|
|
3
|
+
this.element = element
|
|
4
|
+
this.listElement = listElement || element?.querySelector('[data-popup-list]') || element?.querySelector('ul')
|
|
5
|
+
this.onSelect = onSelect || (() => { })
|
|
6
|
+
this.renderItem = renderItem || ((item) => item?.label || '')
|
|
7
|
+
this.onClose = onClose
|
|
8
|
+
this.closeOnOutsideClick = closeOnOutsideClick
|
|
9
|
+
this.items = []
|
|
10
|
+
this.activeIndex = -1
|
|
11
|
+
|
|
12
|
+
this.handleOutsideClick = this.handleOutsideClick.bind(this)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
showAt(anchorRect) {
|
|
16
|
+
if (!this.element) return
|
|
17
|
+
|
|
18
|
+
this.element.style.display = 'block'
|
|
19
|
+
this.element.style.visibility = 'hidden'
|
|
20
|
+
|
|
21
|
+
requestAnimationFrame(() => {
|
|
22
|
+
this.updatePosition(anchorRect)
|
|
23
|
+
this.element.style.visibility = 'visible'
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (this.closeOnOutsideClick) {
|
|
27
|
+
document.addEventListener('mousedown', this.handleOutsideClick)
|
|
28
|
+
document.addEventListener('touchstart', this.handleOutsideClick)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
updatePosition(anchorRect) {
|
|
33
|
+
if (!this.element) return
|
|
34
|
+
|
|
35
|
+
const scrollX = window.scrollX || window.pageXOffset || 0
|
|
36
|
+
const scrollY = window.scrollY || window.pageYOffset || 0
|
|
37
|
+
const offsetParent = this.element.offsetParent
|
|
38
|
+
const parentRect = offsetParent?.getBoundingClientRect?.() || { left: 0, top: 0 }
|
|
39
|
+
const parentScrollX = offsetParent?.scrollLeft || 0
|
|
40
|
+
const parentScrollY = offsetParent?.scrollTop || 0
|
|
41
|
+
const boundsPadding = 8
|
|
42
|
+
const rect = anchorRect || this.element.getBoundingClientRect()
|
|
43
|
+
|
|
44
|
+
let viewportLeft = (rect?.left || 0)
|
|
45
|
+
let viewportTop = (rect?.bottom || 0) + 4
|
|
46
|
+
|
|
47
|
+
const { offsetWidth: width, offsetHeight: height } = this.element
|
|
48
|
+
const maxLeft = window.innerWidth - width - boundsPadding
|
|
49
|
+
const maxTop = window.innerHeight - height - boundsPadding
|
|
50
|
+
|
|
51
|
+
viewportLeft = Math.max(boundsPadding, Math.min(viewportLeft, maxLeft))
|
|
52
|
+
viewportTop = Math.max(boundsPadding, Math.min(viewportTop, maxTop))
|
|
53
|
+
|
|
54
|
+
const left = viewportLeft - parentRect.left + parentScrollX
|
|
55
|
+
const top = viewportTop - parentRect.top + parentScrollY
|
|
56
|
+
|
|
57
|
+
this.element.style.left = `${left}px`
|
|
58
|
+
this.element.style.top = `${top}px`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setItems(items = []) {
|
|
62
|
+
this.items = items
|
|
63
|
+
if (!this.listElement) return
|
|
64
|
+
|
|
65
|
+
this.listElement.innerHTML = ''
|
|
66
|
+
items.forEach((item, index) => {
|
|
67
|
+
const li = document.createElement('li')
|
|
68
|
+
li.className = 'common-popup-item'
|
|
69
|
+
li.dataset.index = String(index)
|
|
70
|
+
li.innerHTML = this.renderItem(item, index)
|
|
71
|
+
li.addEventListener('mouseenter', () => this.setActiveIndex(index))
|
|
72
|
+
li.addEventListener('mousedown', (event) => event.preventDefault())
|
|
73
|
+
li.addEventListener('click', () => this.handleItemSelect(index))
|
|
74
|
+
|
|
75
|
+
// Handle touch events for mobile stability (prevent keyboard dismissal/double-tap issues)
|
|
76
|
+
let touchStartY = null
|
|
77
|
+
li.addEventListener('touchstart', (e) => {
|
|
78
|
+
touchStartY = e.touches[0].clientY
|
|
79
|
+
}, { passive: false })
|
|
80
|
+
|
|
81
|
+
li.addEventListener('touchend', (e) => {
|
|
82
|
+
if (touchStartY === null) return
|
|
83
|
+
const diffY = Math.abs(e.changedTouches[0].clientY - touchStartY)
|
|
84
|
+
if (diffY < 10) { // Threshold for tap vs scroll
|
|
85
|
+
e.preventDefault()
|
|
86
|
+
this.handleItemSelect(index)
|
|
87
|
+
}
|
|
88
|
+
touchStartY = null
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
this.listElement.appendChild(li)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
this.activeIndex = items.length > 0 ? 0 : -1
|
|
95
|
+
this.updateActiveItem()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
handleItemSelect(index) {
|
|
99
|
+
if (index < 0 || index >= this.items.length) return
|
|
100
|
+
this.activeIndex = index
|
|
101
|
+
this.updateActiveItem()
|
|
102
|
+
const item = this.items[index]
|
|
103
|
+
if (item) {
|
|
104
|
+
this.onSelect(item)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
setActiveIndex(index) {
|
|
109
|
+
if (this.items.length === 0) {
|
|
110
|
+
this.activeIndex = -1
|
|
111
|
+
this.updateActiveItem()
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (index < 0) {
|
|
116
|
+
this.activeIndex = this.items.length - 1
|
|
117
|
+
} else {
|
|
118
|
+
this.activeIndex = index % this.items.length
|
|
119
|
+
}
|
|
120
|
+
this.updateActiveItem()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
updateActiveItem() {
|
|
124
|
+
if (!this.listElement) return
|
|
125
|
+
const items = Array.from(this.listElement.children)
|
|
126
|
+
items.forEach((item, index) => {
|
|
127
|
+
item.classList.toggle('active', index === this.activeIndex)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const activeItem = items[this.activeIndex]
|
|
131
|
+
if (activeItem && activeItem.scrollIntoView) {
|
|
132
|
+
activeItem.scrollIntoView({ block: 'nearest' })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
handleKey(event) {
|
|
137
|
+
if (!this.isOpen() || this.items.length === 0) return false
|
|
138
|
+
|
|
139
|
+
const key = event.key
|
|
140
|
+
const isCtrl = event.ctrlKey || event.metaKey
|
|
141
|
+
const lowered = key?.toLowerCase?.() || key
|
|
142
|
+
|
|
143
|
+
if (key === 'Tab' || key === 'Enter') {
|
|
144
|
+
event.preventDefault()
|
|
145
|
+
this.handleItemSelect(this.activeIndex)
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (key === 'ArrowDown' || (isCtrl && lowered === 'n')) {
|
|
150
|
+
event.preventDefault()
|
|
151
|
+
this.setActiveIndex(this.activeIndex + 1)
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (key === 'ArrowUp' || (isCtrl && lowered === 'p')) {
|
|
156
|
+
event.preventDefault()
|
|
157
|
+
this.setActiveIndex(this.activeIndex - 1)
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (key === 'Escape') {
|
|
162
|
+
this.hide('escape')
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
hide(reason = 'manual') {
|
|
170
|
+
if (!this.element || !this.isOpen()) return
|
|
171
|
+
this.element.style.display = 'none'
|
|
172
|
+
this.element.style.visibility = ''
|
|
173
|
+
this.items = []
|
|
174
|
+
this.activeIndex = -1
|
|
175
|
+
this.updateActiveItem()
|
|
176
|
+
|
|
177
|
+
document.removeEventListener('mousedown', this.handleOutsideClick)
|
|
178
|
+
document.removeEventListener('touchstart', this.handleOutsideClick)
|
|
179
|
+
|
|
180
|
+
if (typeof this.onClose === 'function') {
|
|
181
|
+
this.onClose(reason)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
handleOutsideClick(event) {
|
|
186
|
+
if (!this.element) return
|
|
187
|
+
if (!this.element.contains(event.target)) {
|
|
188
|
+
this.hide('outside')
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
isOpen() {
|
|
193
|
+
return this.element && this.element.style.display === 'block'
|
|
194
|
+
}
|
|
195
|
+
}
|