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,413 @@
|
|
|
1
|
+
require "ostruct"
|
|
2
|
+
require "closure_tree"
|
|
3
|
+
|
|
4
|
+
module Collavre
|
|
5
|
+
class Creative < ApplicationRecord
|
|
6
|
+
self.table_name = "creatives"
|
|
7
|
+
|
|
8
|
+
# Use non-namespaced partial path for backward compatibility
|
|
9
|
+
def to_partial_path
|
|
10
|
+
"creatives/creative"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
after_save :touch_subtree_on_move, if: :saved_change_to_parent_id?
|
|
14
|
+
|
|
15
|
+
unless const_defined?(:DEFAULT_GITHUB_GEMINI_PROMPT)
|
|
16
|
+
DEFAULT_GITHUB_GEMINI_PROMPT = <<~PROMPT.freeze
|
|
17
|
+
You are reviewing a GitHub pull request and mapping it to Creative tasks.
|
|
18
|
+
Pull request title: \#{pr_title}
|
|
19
|
+
Pull request body:
|
|
20
|
+
\#{pr_body}
|
|
21
|
+
|
|
22
|
+
Pull request commit messages:
|
|
23
|
+
\#{commit_messages}
|
|
24
|
+
|
|
25
|
+
Pull request diff:
|
|
26
|
+
\#{diff}
|
|
27
|
+
|
|
28
|
+
Creative tree structure. Each line represents a creative node with indentation indicating depth (4 spaces per level).
|
|
29
|
+
Format: - {"id": <ID>, "progress": <0.0-1.0>, "desc": "<Description>"}
|
|
30
|
+
\#{creative_tree}
|
|
31
|
+
|
|
32
|
+
\#{language_instructions}
|
|
33
|
+
|
|
34
|
+
When describing creatives, write from an end-user perspective similar to a user manual. Avoid unnecessary technical detail, and keep sentences concise.
|
|
35
|
+
|
|
36
|
+
Return a JSON object with two keys:
|
|
37
|
+
- "completed": array of objects representing tasks finished by this PR. Each object must include "creative_id" (from the IDs above). Use only creatives marked [LEAF] in the list above. Optionally include "progress" (0.0 to 1.0), "note", or "path" for context.
|
|
38
|
+
- "additional": array of objects for new creatives that are not already represented in the tree above. Each object must include "parent_id" (from the IDs above) and "description" (the new creative text). Do not use this list for follow-up tasks on existing creatives—only describe brand new creatives. Optionally include "progress" (0.0 to 1.0), "note", or "path".
|
|
39
|
+
|
|
40
|
+
Do not add tasks to "completed" if they already show 100% progress in the tree above unless this PR clearly made new changes that justify marking them complete.
|
|
41
|
+
|
|
42
|
+
Use only IDs present in the tree. Respond with valid JSON only.
|
|
43
|
+
PROMPT
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
has_many :comments, class_name: "Collavre::Comment", dependent: :destroy
|
|
47
|
+
has_many :comment_read_pointers, class_name: "Collavre::CommentReadPointer", dependent: :delete_all
|
|
48
|
+
|
|
49
|
+
has_closure_tree order: :sequence, name_column: :description, hierarchy_table_name: "creative_hierarchies"
|
|
50
|
+
|
|
51
|
+
attr_accessor :filtered_progress
|
|
52
|
+
|
|
53
|
+
belongs_to :origin, class_name: "Collavre::Creative", optional: true
|
|
54
|
+
has_many :linked_creatives, class_name: "Collavre::Creative", foreign_key: :origin_id, dependent: :delete_all
|
|
55
|
+
belongs_to :user, class_name: Collavre.configuration.user_class_name, optional: true
|
|
56
|
+
|
|
57
|
+
has_many :creative_shares, class_name: "Collavre::CreativeShare", dependent: :destroy
|
|
58
|
+
has_many :creative_shares_caches, class_name: "Collavre::CreativeSharesCache", dependent: :delete_all
|
|
59
|
+
has_many :tags, class_name: "Collavre::Tag", dependent: :destroy
|
|
60
|
+
has_many :creative_expanded_states, class_name: "Collavre::CreativeExpandedState", dependent: :delete_all
|
|
61
|
+
has_many :invitations, class_name: "Collavre::Invitation", dependent: :delete_all
|
|
62
|
+
has_many :github_repository_links, dependent: :destroy
|
|
63
|
+
has_many :notion_page_links, dependent: :destroy
|
|
64
|
+
has_many :notion_block_links, dependent: :destroy
|
|
65
|
+
has_many :topics, class_name: "Collavre::Topic", dependent: :destroy
|
|
66
|
+
has_many :mcp_tools, dependent: :destroy
|
|
67
|
+
has_many :activity_logs, class_name: "Collavre::ActivityLog", dependent: :destroy
|
|
68
|
+
has_many :calendar_events, class_name: "Collavre::CalendarEvent", dependent: :destroy
|
|
69
|
+
has_many :labels, class_name: "Collavre::Label", dependent: :destroy
|
|
70
|
+
|
|
71
|
+
validates :progress, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 1.0 }, unless: -> { origin_id.present? }
|
|
72
|
+
validates :description, presence: true, unless: -> { origin_id.present? }
|
|
73
|
+
|
|
74
|
+
validate :progress_cannot_change_if_has_origin, on: :update
|
|
75
|
+
validate :description_cannot_change_if_has_origin, on: :update
|
|
76
|
+
validate :origin_cannot_be_self
|
|
77
|
+
|
|
78
|
+
before_validation :assign_default_user, on: :create
|
|
79
|
+
before_validation :redirect_parent_to_origin
|
|
80
|
+
before_save :sanitize_description_html
|
|
81
|
+
|
|
82
|
+
after_save :update_parent_progress
|
|
83
|
+
after_destroy :update_parent_progress
|
|
84
|
+
after_destroy_commit :purge_description_attachments
|
|
85
|
+
after_save :update_mcp_tools
|
|
86
|
+
|
|
87
|
+
after_commit :rebuild_permission_cache, if: :saved_change_to_parent_id?, unless: :destroyed?
|
|
88
|
+
after_commit :cache_owner_permission, on: :create
|
|
89
|
+
after_commit :update_owner_cache, if: :saved_change_to_user_id?, unless: :destroyed?
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def has_permission?(user, required_permission = :read)
|
|
95
|
+
Collavre::Creatives::PermissionChecker.new(self, user).allowed?(required_permission)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns only children for which the user has at least the given permission (default: :read)
|
|
99
|
+
def children_with_permission(user = nil, min_permission = :read)
|
|
100
|
+
user ||= Collavre.current_user
|
|
101
|
+
children_scope = effective_origin(Set.new).children
|
|
102
|
+
children_ids = children_scope.pluck(:id)
|
|
103
|
+
return [] if children_ids.empty?
|
|
104
|
+
|
|
105
|
+
min_rank = CreativeShare.permissions[min_permission.to_s]
|
|
106
|
+
accessible_ids = Set.new
|
|
107
|
+
|
|
108
|
+
if user
|
|
109
|
+
# 사용자별 엔트리 확인 (user-specific entry가 있으면 public share보다 우선)
|
|
110
|
+
user_entries = CreativeSharesCache
|
|
111
|
+
.where(creative_id: children_ids, user_id: user.id)
|
|
112
|
+
.pluck(:creative_id, :permission)
|
|
113
|
+
|
|
114
|
+
user_has_entry = Set.new # user-specific entry가 있는 children
|
|
115
|
+
user_entries.each do |cid, perm|
|
|
116
|
+
user_has_entry << cid
|
|
117
|
+
# perm is string from enum, convert to integer for comparison
|
|
118
|
+
perm_rank = CreativeSharesCache.permissions[perm]
|
|
119
|
+
# User-specific entry with sufficient permission grants access
|
|
120
|
+
if perm_rank && perm_rank >= min_rank && perm_rank != CreativeSharesCache.permissions[:no_access]
|
|
121
|
+
accessible_ids << cid
|
|
122
|
+
end
|
|
123
|
+
# If insufficient or no_access, don't add to accessible - will be excluded from public fallback
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Public share 확인 (user-specific entry가 있는 건 제외 - user entry가 우선)
|
|
127
|
+
public_accessible = CreativeSharesCache
|
|
128
|
+
.where(creative_id: children_ids, user_id: nil)
|
|
129
|
+
.where("permission >= ?", min_rank)
|
|
130
|
+
.where.not(permission: :no_access)
|
|
131
|
+
.pluck(:creative_id)
|
|
132
|
+
accessible_ids.merge(public_accessible - user_has_entry.to_a)
|
|
133
|
+
|
|
134
|
+
# Fallback: include owned children (for fixtures and missing cache entries)
|
|
135
|
+
owned_ids = children_scope.where(user_id: user.id).pluck(:id)
|
|
136
|
+
accessible_ids.merge(owned_ids)
|
|
137
|
+
else
|
|
138
|
+
# 비로그인: public share만
|
|
139
|
+
accessible_ids = CreativeSharesCache
|
|
140
|
+
.where(creative_id: children_ids, user_id: nil)
|
|
141
|
+
.where("permission >= ?", min_rank)
|
|
142
|
+
.where.not(permission: :no_access)
|
|
143
|
+
.pluck(:creative_id)
|
|
144
|
+
.to_set
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
children_scope.where(id: accessible_ids.to_a).order(:sequence).to_a
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns the effective attribute for linked creatives
|
|
151
|
+
def effective_attribute(attr, visited_ids = Set.new)
|
|
152
|
+
return self[attr] if origin_id.nil? || attr.to_s == "parent_id"
|
|
153
|
+
return self[attr] if visited_ids.include?(id)
|
|
154
|
+
|
|
155
|
+
visited_ids.add(id)
|
|
156
|
+
origin.effective_attribute(attr, visited_ids)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def effective_origin(visited_ids = Set.new)
|
|
160
|
+
return self if origin_id.nil?
|
|
161
|
+
return self if visited_ids.include?(id)
|
|
162
|
+
|
|
163
|
+
visited_ids.add(id)
|
|
164
|
+
origin.effective_origin(visited_ids)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Compatibility helper: ancestry gem exposes `subtree_ids`, while
|
|
168
|
+
# closure_tree typically uses `self_and_descendants`.
|
|
169
|
+
# Provide `subtree_ids` so call sites (e.g., controller search) work.
|
|
170
|
+
def subtree_ids
|
|
171
|
+
self_and_descendants.pluck(:id)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Linked Creative의 description을 안전하게 반환
|
|
175
|
+
# variation_id가 주어지면 해당 Variation의 Tag value를 반환, 없으면 기존 description 반환
|
|
176
|
+
def effective_description(variation_id = nil, html = true)
|
|
177
|
+
if variation_id.present?
|
|
178
|
+
variation_tag = tags.find_by(label_id: variation_id)
|
|
179
|
+
return variation_tag.value if variation_tag&.value.present?
|
|
180
|
+
end
|
|
181
|
+
if origin_id.nil?
|
|
182
|
+
description_val = description
|
|
183
|
+
else
|
|
184
|
+
description_val = origin.description
|
|
185
|
+
end
|
|
186
|
+
if html
|
|
187
|
+
description_val&.to_s || ""
|
|
188
|
+
else
|
|
189
|
+
# For plain text, we might need to strip tags if description is HTML
|
|
190
|
+
# But the original code used rich_text_description&.body which returns ActionText::Content
|
|
191
|
+
# which has to_s (HTML) and to_plain_text.
|
|
192
|
+
# Since we now store raw HTML, we should strip tags for plain text.
|
|
193
|
+
ActionController::Base.helpers.strip_tags(description_val&.to_s || "")
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def creative_snippet
|
|
198
|
+
ActionController::Base.helpers.strip_tags(effective_origin.description || "").truncate(24, omission: "...")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def progress
|
|
202
|
+
effective_attribute(:progress, Set.new)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def user
|
|
206
|
+
target = effective_origin(Set.new)
|
|
207
|
+
return super if target == self
|
|
208
|
+
|
|
209
|
+
target.user
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def children
|
|
213
|
+
# better not override this method, use children_with_permission instead or linked_children
|
|
214
|
+
super
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def linked_children
|
|
218
|
+
origin_id.nil? ? children_with_permission(Collavre.current_user, :read) : origin&.children_with_permission(Collavre.current_user, :read) || []
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def owning_parent
|
|
222
|
+
if parent.present?
|
|
223
|
+
Creative.find_by(origin_id: parent.id, user: Collavre.current_user) || parent
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def prompt_for(user)
|
|
228
|
+
comments
|
|
229
|
+
.where(private: true, user: user)
|
|
230
|
+
.where("content LIKE ?", "> %")
|
|
231
|
+
.order(created_at: :desc)
|
|
232
|
+
.first
|
|
233
|
+
&.content
|
|
234
|
+
&.sub(/\A>\s*/i, "")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def progress_for_tags(tag_ids, user = Collavre.current_user)
|
|
238
|
+
progress_service.progress_for_tags(tag_ids, user)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Calculate progress for the subtree ignoring permission checks.
|
|
242
|
+
# `tagged_ids` should be a Set of creative IDs that are tagged with the plan.
|
|
243
|
+
def progress_for_plan(tagged_ids)
|
|
244
|
+
progress_service.progress_for_plan(tagged_ids)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# 공유 대상 사용자를 위해 Linked Creative를 생성합니다.
|
|
248
|
+
# 이미 존재하거나 원본 작성자에게는 생성하지 않습니다.
|
|
249
|
+
def create_linked_creative_for_user(user)
|
|
250
|
+
original = effective_origin(Set.new)
|
|
251
|
+
return if original.user_id == user.id
|
|
252
|
+
ancestor_ids = original.ancestors.pluck(:id)
|
|
253
|
+
has_ancestor_share = CreativeShare.where(creative_id: ancestor_ids, user_id: user.id)
|
|
254
|
+
.where.not(permission: :no_access)
|
|
255
|
+
.exists?
|
|
256
|
+
has_owning_ancestors = Creative.where(id: ancestor_ids, user_id: user.id)
|
|
257
|
+
.exists?
|
|
258
|
+
return if has_ancestor_share or has_owning_ancestors
|
|
259
|
+
Creative.find_or_create_by!(origin_id: original.id, user_id: user.id) do |c|
|
|
260
|
+
c.parent_id = nil
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def update_parent_progress
|
|
265
|
+
progress_service.update_parent_progress!
|
|
266
|
+
|
|
267
|
+
if saved_change_to_parent_id?
|
|
268
|
+
old_parent_id = saved_change_to_parent_id[0]
|
|
269
|
+
if old_parent_id && (old_parent = Creative.find_by(id: old_parent_id))
|
|
270
|
+
Collavre::Creatives::ProgressService.new(old_parent).update_progress_from_children!
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def all_shared_users(required_permission = :no_access)
|
|
276
|
+
base_creative = effective_origin(Set.new)
|
|
277
|
+
ancestor_ids = [ base_creative.id ] + base_creative.ancestors.pluck(:id)
|
|
278
|
+
required_permission_level = CreativeShare.permissions.fetch(required_permission.to_s)
|
|
279
|
+
|
|
280
|
+
shares = CreativeShare.where(creative_id: ancestor_ids).includes(:user)
|
|
281
|
+
shares_for_user_hash = shares.group_by(&:user_id)
|
|
282
|
+
|
|
283
|
+
shares_for_user_hash.filter_map do |_user_id, user_shares|
|
|
284
|
+
closest_share = CreativeShare.closest_parent_share(ancestor_ids, user_shares)
|
|
285
|
+
next unless closest_share
|
|
286
|
+
|
|
287
|
+
closest_permission_level = CreativeShare.permissions.fetch(closest_share.permission.to_s)
|
|
288
|
+
next if closest_permission_level < required_permission_level
|
|
289
|
+
|
|
290
|
+
closest_share
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def github_gemini_prompt_template
|
|
295
|
+
github_gemini_prompt.presence || DEFAULT_GITHUB_GEMINI_PROMPT
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
private
|
|
299
|
+
|
|
300
|
+
def purge_description_attachments
|
|
301
|
+
return if description.blank?
|
|
302
|
+
|
|
303
|
+
signed_ids = extract_signed_ids_from_description
|
|
304
|
+
return if signed_ids.empty?
|
|
305
|
+
|
|
306
|
+
signed_ids.each do |signed_id|
|
|
307
|
+
begin
|
|
308
|
+
blob = ActiveStorage::Blob.find_signed(signed_id)
|
|
309
|
+
next unless blob
|
|
310
|
+
|
|
311
|
+
# Skip purging if another creative still references the blob
|
|
312
|
+
next if Creative.where.not(id: id)
|
|
313
|
+
.where("description LIKE ?", "%#{ActiveRecord::Base.sanitize_sql_like(signed_id)}%")
|
|
314
|
+
.exists?
|
|
315
|
+
|
|
316
|
+
blob.purge
|
|
317
|
+
rescue ActiveRecord::RecordNotFound, ActiveSupport::MessageVerifier::InvalidSignature
|
|
318
|
+
Rails.logger.warn("Creative##{id}: could not find blob for signed_id=#{signed_id}")
|
|
319
|
+
rescue => e
|
|
320
|
+
Rails.logger.error("Creative##{id}: failed to purge blob #{signed_id}: #{e.message}")
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def sanitize_description_html
|
|
326
|
+
table_tags = %w[table thead tbody tfoot tr th td]
|
|
327
|
+
table_attrs = %w[colspan rowspan]
|
|
328
|
+
self.description = ActionController::Base.helpers.sanitize(
|
|
329
|
+
description,
|
|
330
|
+
tags: Rails::HTML5::SafeListSanitizer.allowed_tags.to_a + table_tags,
|
|
331
|
+
attributes: Rails::HTML5::SafeListSanitizer.allowed_attributes.to_a + table_attrs + %w[data-lexical]
|
|
332
|
+
)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def extract_signed_ids_from_description
|
|
336
|
+
return [] if description.blank?
|
|
337
|
+
|
|
338
|
+
html = description.to_s
|
|
339
|
+
|
|
340
|
+
ids = html.scan(%r{/rails/active_storage/blobs/(?:redirect|proxy)/([^/?#]+)}).flatten
|
|
341
|
+
ids += html.scan(%r{/rails/active_storage/blobs/([^/?#]+)}).flatten
|
|
342
|
+
|
|
343
|
+
ids.uniq
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def assign_default_user
|
|
347
|
+
return if user.present?
|
|
348
|
+
if parent_id.present? && parent
|
|
349
|
+
self.user = parent.user
|
|
350
|
+
else
|
|
351
|
+
self.user = Collavre.current_user
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def progress_service
|
|
356
|
+
@progress_service ||= Collavre::Creatives::ProgressService.new(self)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def redirect_parent_to_origin
|
|
360
|
+
if parent&.origin_id.present?
|
|
361
|
+
self.parent = parent.origin
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def update_mcp_tools
|
|
366
|
+
McpService.new.update_from_creative(self)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def progress_cannot_change_if_has_origin
|
|
370
|
+
if origin_id.present? && will_save_change_to_progress?
|
|
371
|
+
errors.add(:progress, "cannot be changed directly when linked to an origin")
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def description_cannot_change_if_has_origin
|
|
376
|
+
if origin_id.present? && will_save_change_to_description?
|
|
377
|
+
errors.add(:description, "cannot be changed directly when linked to an origin")
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def origin_cannot_be_self
|
|
382
|
+
if origin_id.present? && origin_id == id
|
|
383
|
+
errors.add(:origin_id, "cannot be the same as id")
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def rebuild_permission_cache
|
|
388
|
+
PermissionCacheJob.perform_later(:rebuild_for_creative, creative_id: id)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def cache_owner_permission
|
|
392
|
+
PermissionCacheJob.perform_later(:cache_owner, creative_id: id)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def update_owner_cache
|
|
396
|
+
old_user_id, new_user_id = saved_change_to_user_id
|
|
397
|
+
PermissionCacheJob.perform_later(:update_owner,
|
|
398
|
+
creative_id: id,
|
|
399
|
+
old_user_id: old_user_id,
|
|
400
|
+
new_user_id: new_user_id
|
|
401
|
+
)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
private
|
|
405
|
+
|
|
406
|
+
def touch_subtree_on_move
|
|
407
|
+
# When moving a tree, all descendants might have new effective permissions
|
|
408
|
+
# so we must touch them to invalidate cache.
|
|
409
|
+
# self is already touched by save.
|
|
410
|
+
descendants.update_all(updated_at: Time.current)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
class CreativeExpandedState < ApplicationRecord
|
|
3
|
+
self.table_name = "creative_expanded_states"
|
|
4
|
+
|
|
5
|
+
belongs_to :creative, class_name: "Collavre::Creative", optional: true
|
|
6
|
+
belongs_to :user, class_name: Collavre.configuration.user_class_name
|
|
7
|
+
|
|
8
|
+
validates :expanded_status, presence: true
|
|
9
|
+
validates :creative_id, uniqueness: { scope: :user_id }, allow_nil: true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
class CreativeShare < ApplicationRecord
|
|
3
|
+
self.table_name = "creative_shares"
|
|
4
|
+
|
|
5
|
+
belongs_to :creative, class_name: "Collavre::Creative"
|
|
6
|
+
belongs_to :user, class_name: Collavre.configuration.user_class_name, optional: true
|
|
7
|
+
belongs_to :shared_by, class_name: Collavre.configuration.user_class_name, optional: true
|
|
8
|
+
|
|
9
|
+
enum :permission, {
|
|
10
|
+
no_access: 0,
|
|
11
|
+
read: 1,
|
|
12
|
+
feedback: 2,
|
|
13
|
+
write: 3,
|
|
14
|
+
admin: 4
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
validates :creative_id, presence: true
|
|
18
|
+
validates :user_id, presence: true, unless: -> { user_id.nil? } # Public share has nil user_id
|
|
19
|
+
|
|
20
|
+
validates :permission, presence: true
|
|
21
|
+
validates :user_id, uniqueness: { scope: :creative_id }, allow_nil: true
|
|
22
|
+
|
|
23
|
+
after_create_commit :notify_recipient, unless: :no_access?
|
|
24
|
+
after_save :touch_creative_subtree
|
|
25
|
+
after_destroy :touch_creative_subtree
|
|
26
|
+
|
|
27
|
+
after_commit :propagate_cache, on: [ :create, :update ]
|
|
28
|
+
after_destroy_commit :remove_cache
|
|
29
|
+
|
|
30
|
+
# Given ancestor_ids and ancestor_shares, returns the closest CreativeShare
|
|
31
|
+
# in the ancestors. If there is no ancestor share, returns nil.
|
|
32
|
+
def self.closest_parent_share(ancestor_ids, ancestor_shares)
|
|
33
|
+
ancestor_shares.to_a.min_by { |s| ancestor_ids.index(s.creative_id) || Float::INFINITY }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def sharer_id
|
|
37
|
+
shared_by_id || creative.user_id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def touch_creative_subtree
|
|
43
|
+
creatives_to_touch = []
|
|
44
|
+
|
|
45
|
+
# Current creative
|
|
46
|
+
creatives_to_touch << creative if creative
|
|
47
|
+
|
|
48
|
+
# Old creative if changed (check before_last_save because we are in after_save)
|
|
49
|
+
if saved_change_to_creative_id?
|
|
50
|
+
old_id = creative_id_before_last_save
|
|
51
|
+
if old_id && old_id != creative_id
|
|
52
|
+
creatives_to_touch << Creative.find_by(id: old_id)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
creatives_to_touch.compact.uniq.each do |c|
|
|
57
|
+
timestamp = Time.current
|
|
58
|
+
c.touch
|
|
59
|
+
c.descendants.update_all(updated_at: timestamp)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def notify_recipient
|
|
64
|
+
return unless Current.user && user
|
|
65
|
+
desc = creative.effective_description
|
|
66
|
+
title = ActionController::Base.helpers.strip_tags(desc)
|
|
67
|
+
short_title = ActionController::Base.helpers.truncate(title, length: 30)
|
|
68
|
+
InboxItem.create!(
|
|
69
|
+
owner: user,
|
|
70
|
+
message_key: "inbox.creative_shared",
|
|
71
|
+
message_params: { user: Current.user.display_name, short_title: short_title },
|
|
72
|
+
link: Collavre::Engine.routes.url_helpers.creative_url(
|
|
73
|
+
creative,
|
|
74
|
+
Rails.application.config.action_mailer.default_url_options
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def linked_creative_exists?
|
|
80
|
+
Creative.exists?(origin_id: creative.id, user_id: user.id)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def propagate_cache
|
|
84
|
+
# If creative_id or user_id changed, handle old cache entries properly
|
|
85
|
+
if saved_change_to_creative_id? || saved_change_to_user_id?
|
|
86
|
+
# Delete only caches created by THIS share (fast operation, keep synchronous)
|
|
87
|
+
CreativeSharesCache.where(source_share_id: id).delete_all
|
|
88
|
+
|
|
89
|
+
# Rebuild caches for old user in old subtree (background job)
|
|
90
|
+
if saved_change_to_creative_id?
|
|
91
|
+
old_creative_id = creative_id_before_last_save
|
|
92
|
+
old_user_id = user_id_before_last_save || user_id
|
|
93
|
+
if old_creative_id
|
|
94
|
+
PermissionCacheJob.perform_later(:rebuild_user_cache_for_subtree,
|
|
95
|
+
creative_id: old_creative_id,
|
|
96
|
+
user_id: old_user_id
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
elsif saved_change_to_user_id?
|
|
100
|
+
old_user_id = user_id_before_last_save
|
|
101
|
+
if old_user_id
|
|
102
|
+
PermissionCacheJob.perform_later(:rebuild_user_cache_for_subtree,
|
|
103
|
+
creative_id: creative_id,
|
|
104
|
+
user_id: old_user_id
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
PermissionCacheJob.perform_later(:propagate_share, creative_share_id: id)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def remove_cache
|
|
114
|
+
# Capture IDs before destroy (after_destroy still has access to attributes)
|
|
115
|
+
PermissionCacheJob.perform_later(:remove_share,
|
|
116
|
+
creative_share_id: id,
|
|
117
|
+
creative_id: creative_id,
|
|
118
|
+
user_id: user_id
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
class CreativeSharesCache < ApplicationRecord
|
|
3
|
+
self.table_name = "creative_shares_caches"
|
|
4
|
+
self.record_timestamps = false
|
|
5
|
+
|
|
6
|
+
belongs_to :creative, class_name: "Collavre::Creative"
|
|
7
|
+
belongs_to :user, class_name: Collavre.configuration.user_class_name, optional: true
|
|
8
|
+
belongs_to :source_share, class_name: "Collavre::CreativeShare", optional: true
|
|
9
|
+
|
|
10
|
+
enum :permission, {
|
|
11
|
+
no_access: 0,
|
|
12
|
+
read: 1,
|
|
13
|
+
feedback: 2,
|
|
14
|
+
write: 3,
|
|
15
|
+
admin: 4
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
5
|
+
attribute :session
|
|
6
|
+
attribute :creative_share_cache
|
|
7
|
+
attribute :mcp_tool_approval_required
|
|
8
|
+
attribute :user
|
|
9
|
+
|
|
10
|
+
def user
|
|
11
|
+
super || session&.user
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
class Device < ApplicationRecord
|
|
3
|
+
self.table_name = "devices"
|
|
4
|
+
|
|
5
|
+
enum :device_type, { web: 0, pwa: 1, android: 2, ios: 3 }
|
|
6
|
+
|
|
7
|
+
belongs_to :user, class_name: Collavre.configuration.user_class_name
|
|
8
|
+
|
|
9
|
+
validates :client_id, presence: true, uniqueness: true
|
|
10
|
+
validates :device_type, presence: true
|
|
11
|
+
validates :fcm_token, presence: true, uniqueness: true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
class Email < ApplicationRecord
|
|
3
|
+
self.table_name = "emails"
|
|
4
|
+
|
|
5
|
+
belongs_to :user, class_name: Collavre.configuration.user_class_name, optional: true
|
|
6
|
+
|
|
7
|
+
enum :event, {
|
|
8
|
+
invitation: "invitation",
|
|
9
|
+
inbox_summary: "inbox_summary"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
validates :email, :subject, :event, presence: true
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
class GithubAccount < ApplicationRecord
|
|
3
|
+
self.table_name = "github_accounts"
|
|
4
|
+
|
|
5
|
+
belongs_to :user, class_name: "Collavre::User"
|
|
6
|
+
has_many :github_repository_links, class_name: "Collavre::GithubRepositoryLink", dependent: :destroy
|
|
7
|
+
|
|
8
|
+
encrypts :token, deterministic: false
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
class GithubRepositoryLink < ApplicationRecord
|
|
3
|
+
self.table_name = "github_repository_links"
|
|
4
|
+
|
|
5
|
+
belongs_to :creative, class_name: "Collavre::Creative"
|
|
6
|
+
belongs_to :github_account, class_name: "Collavre::GithubAccount"
|
|
7
|
+
|
|
8
|
+
validates :repository_full_name, presence: true
|
|
9
|
+
validates :webhook_secret, presence: true
|
|
10
|
+
|
|
11
|
+
before_validation :ensure_webhook_secret
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def ensure_webhook_secret
|
|
16
|
+
self.webhook_secret ||= SecureRandom.hex(20)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|