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.
Files changed (410) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +221 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/stylesheets/collavre/actiontext.css +577 -0
  5. data/app/assets/stylesheets/collavre/activity_logs.css +99 -0
  6. data/app/assets/stylesheets/collavre/comments_popup.css +692 -0
  7. data/app/assets/stylesheets/collavre/creatives.css +559 -0
  8. data/app/assets/stylesheets/collavre/dark_mode.css +118 -0
  9. data/app/assets/stylesheets/collavre/mention_menu.css +43 -0
  10. data/app/assets/stylesheets/collavre/popup.css +160 -0
  11. data/app/assets/stylesheets/collavre/print.css +37 -0
  12. data/app/assets/stylesheets/collavre/slide_view.css +79 -0
  13. data/app/assets/stylesheets/collavre/user_menu.css +34 -0
  14. data/app/channels/collavre/comments_presence_channel.rb +54 -0
  15. data/app/channels/collavre/slide_view_channel.rb +11 -0
  16. data/app/channels/collavre/topics_channel.rb +12 -0
  17. data/app/components/collavre/avatar_component.html.erb +15 -0
  18. data/app/components/collavre/avatar_component.rb +59 -0
  19. data/app/components/collavre/inbox/badge_component.html.erb +6 -0
  20. data/app/components/collavre/inbox/badge_component.rb +18 -0
  21. data/app/components/collavre/plans_timeline_component.html.erb +14 -0
  22. data/app/components/collavre/plans_timeline_component.rb +56 -0
  23. data/app/components/collavre/popup_menu_component.html.erb +6 -0
  24. data/app/components/collavre/popup_menu_component.rb +30 -0
  25. data/app/components/collavre/progress_filter_component.html.erb +5 -0
  26. data/app/components/collavre/progress_filter_component.rb +10 -0
  27. data/app/components/collavre/user_mention_menu_component.html.erb +3 -0
  28. data/app/components/collavre/user_mention_menu_component.rb +8 -0
  29. data/app/controllers/collavre/application_controller.rb +15 -0
  30. data/app/controllers/collavre/attachments_controller.rb +44 -0
  31. data/app/controllers/collavre/calendar_events_controller.rb +15 -0
  32. data/app/controllers/collavre/comment_read_pointers_controller.rb +80 -0
  33. data/app/controllers/collavre/comments/activity_logs_controller.rb +26 -0
  34. data/app/controllers/collavre/comments/reactions_controller.rb +82 -0
  35. data/app/controllers/collavre/comments_controller.rb +464 -0
  36. data/app/controllers/collavre/contacts_controller.rb +10 -0
  37. data/app/controllers/collavre/creative_expanded_states_controller.rb +27 -0
  38. data/app/controllers/collavre/creative_imports_controller.rb +24 -0
  39. data/app/controllers/collavre/creative_plans_controller.rb +69 -0
  40. data/app/controllers/collavre/creative_shares_controller.rb +79 -0
  41. data/app/controllers/collavre/creatives_controller.rb +535 -0
  42. data/app/controllers/collavre/devices_controller.rb +19 -0
  43. data/app/controllers/collavre/email_verifications_controller.rb +16 -0
  44. data/app/controllers/collavre/emails_controller.rb +11 -0
  45. data/app/controllers/collavre/github_auth_controller.rb +25 -0
  46. data/app/controllers/collavre/google_auth_controller.rb +43 -0
  47. data/app/controllers/collavre/inbox_items_controller.rb +64 -0
  48. data/app/controllers/collavre/invites_controller.rb +27 -0
  49. data/app/controllers/collavre/notion_auth_controller.rb +25 -0
  50. data/app/controllers/collavre/passwords_controller.rb +37 -0
  51. data/app/controllers/collavre/plans_controller.rb +110 -0
  52. data/app/controllers/collavre/sessions_controller.rb +57 -0
  53. data/app/controllers/collavre/topics_controller.rb +58 -0
  54. data/app/controllers/collavre/user_themes_controller.rb +58 -0
  55. data/app/controllers/collavre/users_controller.rb +390 -0
  56. data/app/helpers/collavre/application_helper.rb +4 -0
  57. data/app/helpers/collavre/comments_helper.rb +9 -0
  58. data/app/helpers/collavre/creatives_helper.rb +343 -0
  59. data/app/helpers/collavre/navigation_helper.rb +163 -0
  60. data/app/helpers/collavre/user_themes_helper.rb +4 -0
  61. data/app/javascript/collavre.js +26 -0
  62. data/app/javascript/components/InlineLexicalEditor.jsx +889 -0
  63. data/app/javascript/components/LinkPopup.jsx +112 -0
  64. data/app/javascript/components/creative_tree_row.js +503 -0
  65. data/app/javascript/components/plugins/attachment_cleanup_plugin.jsx +95 -0
  66. data/app/javascript/components/plugins/image_upload_plugin.jsx +162 -0
  67. data/app/javascript/controllers/click_target_controller.js +13 -0
  68. data/app/javascript/controllers/comment_controller.js +162 -0
  69. data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +68 -0
  70. data/app/javascript/controllers/comments/form_controller.js +530 -0
  71. data/app/javascript/controllers/comments/list_controller.js +715 -0
  72. data/app/javascript/controllers/comments/mention_menu_controller.js +41 -0
  73. data/app/javascript/controllers/comments/popup_controller.js +385 -0
  74. data/app/javascript/controllers/comments/presence_controller.js +311 -0
  75. data/app/javascript/controllers/comments/topics_controller.js +338 -0
  76. data/app/javascript/controllers/common_popup_controller.js +55 -0
  77. data/app/javascript/controllers/creatives/drag_drop_controller.js +45 -0
  78. data/app/javascript/controllers/creatives/expansion_controller.js +222 -0
  79. data/app/javascript/controllers/creatives/import_controller.js +116 -0
  80. data/app/javascript/controllers/creatives/row_editor_controller.js +8 -0
  81. data/app/javascript/controllers/creatives/select_mode_controller.js +231 -0
  82. data/app/javascript/controllers/creatives/set_plan_modal_controller.js +107 -0
  83. data/app/javascript/controllers/creatives/tree_controller.js +218 -0
  84. data/app/javascript/controllers/index.js +79 -0
  85. data/app/javascript/controllers/link_creative_controller.js +91 -0
  86. data/app/javascript/controllers/popup_menu_controller.js +82 -0
  87. data/app/javascript/controllers/progress_filter_controller.js +35 -0
  88. data/app/javascript/controllers/reaction_picker_controller.js +107 -0
  89. data/app/javascript/controllers/share_invite_controller.js +15 -0
  90. data/app/javascript/controllers/share_user_search_controller.js +121 -0
  91. data/app/javascript/controllers/tabs_controller.js +43 -0
  92. data/app/javascript/creatives/drag_drop/dom.js +170 -0
  93. data/app/javascript/creatives/drag_drop/event_handlers.js +846 -0
  94. data/app/javascript/creatives/drag_drop/indicator.js +35 -0
  95. data/app/javascript/creatives/drag_drop/operations.js +116 -0
  96. data/app/javascript/creatives/drag_drop/state.js +31 -0
  97. data/app/javascript/creatives/tree_renderer.js +248 -0
  98. data/app/javascript/lib/api/__tests__/queue_manager.test.js +153 -0
  99. data/app/javascript/lib/api/creatives.js +79 -0
  100. data/app/javascript/lib/api/csrf_fetch.js +22 -0
  101. data/app/javascript/lib/api/drag_drop.js +31 -0
  102. data/app/javascript/lib/api/queue_manager.js +423 -0
  103. data/app/javascript/lib/apply_lexical_styles.js +15 -0
  104. data/app/javascript/lib/common_popup.js +195 -0
  105. data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +91 -0
  106. data/app/javascript/lib/lexical/__tests__/attachment_payload.test.js +194 -0
  107. data/app/javascript/lib/lexical/action_text_attachment_node.js +459 -0
  108. data/app/javascript/lib/lexical/attachment_node.jsx +170 -0
  109. data/app/javascript/lib/lexical/attachment_payload.js +293 -0
  110. data/app/javascript/lib/lexical/dom_attachment_utils.js +66 -0
  111. data/app/javascript/lib/lexical/image_node.jsx +159 -0
  112. data/app/javascript/lib/lexical/style_attributes.js +40 -0
  113. data/app/javascript/lib/responsive_images.js +54 -0
  114. data/app/javascript/lib/turbo_stream_actions.js +33 -0
  115. data/app/javascript/lib/utils/markdown.js +23 -0
  116. data/app/javascript/modules/creative_guide.js +53 -0
  117. data/app/javascript/modules/creative_row_editor.js +1841 -0
  118. data/app/javascript/modules/creative_row_swipe.js +43 -0
  119. data/app/javascript/modules/creatives.js +15 -0
  120. data/app/javascript/modules/export_to_markdown.js +34 -0
  121. data/app/javascript/modules/inbox_panel.js +226 -0
  122. data/app/javascript/modules/lexical_inline_editor.jsx +133 -0
  123. data/app/javascript/modules/mention_menu.js +77 -0
  124. data/app/javascript/modules/plans_menu.js +39 -0
  125. data/app/javascript/modules/plans_timeline.js +397 -0
  126. data/app/javascript/modules/share_modal.js +73 -0
  127. data/app/javascript/modules/share_user_popup.js +77 -0
  128. data/app/javascript/modules/slide_view.js +163 -0
  129. data/app/javascript/services/cable.js +32 -0
  130. data/app/javascript/slide_view.js +2 -0
  131. data/app/javascript/utils/caret_position.js +42 -0
  132. data/app/javascript/utils/clipboard.js +40 -0
  133. data/app/jobs/collavre/ai_agent_job.rb +27 -0
  134. data/app/jobs/collavre/inbox_summary_job.rb +24 -0
  135. data/app/jobs/collavre/notion_export_job.rb +30 -0
  136. data/app/jobs/collavre/notion_sync_job.rb +48 -0
  137. data/app/jobs/collavre/permission_cache_cleanup_job.rb +36 -0
  138. data/app/jobs/collavre/permission_cache_job.rb +71 -0
  139. data/app/jobs/collavre/push_notification_job.rb +86 -0
  140. data/app/mailers/collavre/application_mailer.rb +17 -0
  141. data/app/mailers/collavre/creative_mailer.rb +9 -0
  142. data/app/mailers/collavre/email_verification_mailer.rb +20 -0
  143. data/app/mailers/collavre/inbox_mailer.rb +19 -0
  144. data/app/mailers/collavre/invitation_mailer.rb +16 -0
  145. data/app/mailers/collavre/passwords_mailer.rb +10 -0
  146. data/app/models/collavre/activity_log.rb +13 -0
  147. data/app/models/collavre/application_record.rb +5 -0
  148. data/app/models/collavre/calendar_event.rb +20 -0
  149. data/app/models/collavre/comment.rb +307 -0
  150. data/app/models/collavre/comment_presence_store.rb +30 -0
  151. data/app/models/collavre/comment_reaction.rb +11 -0
  152. data/app/models/collavre/comment_read_pointer.rb +26 -0
  153. data/app/models/collavre/contact.rb +23 -0
  154. data/app/models/collavre/creative.rb +413 -0
  155. data/app/models/collavre/creative_expanded_state.rb +11 -0
  156. data/app/models/collavre/creative_share.rb +122 -0
  157. data/app/models/collavre/creative_shares_cache.rb +18 -0
  158. data/app/models/collavre/current.rb +14 -0
  159. data/app/models/collavre/device.rb +13 -0
  160. data/app/models/collavre/email.rb +14 -0
  161. data/app/models/collavre/github_account.rb +10 -0
  162. data/app/models/collavre/github_repository_link.rb +19 -0
  163. data/app/models/collavre/inbox_item.rb +95 -0
  164. data/app/models/collavre/invitation.rb +22 -0
  165. data/app/models/collavre/label.rb +47 -0
  166. data/app/models/collavre/mcp_tool.rb +30 -0
  167. data/app/models/collavre/notion_account.rb +17 -0
  168. data/app/models/collavre/notion_block_link.rb +10 -0
  169. data/app/models/collavre/notion_page_link.rb +19 -0
  170. data/app/models/collavre/plan.rb +20 -0
  171. data/app/models/collavre/session.rb +24 -0
  172. data/app/models/collavre/system_setting.rb +144 -0
  173. data/app/models/collavre/tag.rb +10 -0
  174. data/app/models/collavre/task.rb +10 -0
  175. data/app/models/collavre/task_action.rb +10 -0
  176. data/app/models/collavre/topic.rb +12 -0
  177. data/app/models/collavre/user.rb +174 -0
  178. data/app/models/collavre/user_theme.rb +10 -0
  179. data/app/models/collavre/variation.rb +5 -0
  180. data/app/models/collavre/webauthn_credential.rb +11 -0
  181. data/app/services/collavre/ai_agent_service.rb +193 -0
  182. data/app/services/collavre/ai_client.rb +183 -0
  183. data/app/services/collavre/ai_system_prompt_renderer.rb +38 -0
  184. data/app/services/collavre/auto_theme_generator.rb +198 -0
  185. data/app/services/collavre/comment_link_formatter.rb +60 -0
  186. data/app/services/collavre/comments/action_executor.rb +262 -0
  187. data/app/services/collavre/comments/action_validator.rb +58 -0
  188. data/app/services/collavre/comments/calendar_command.rb +97 -0
  189. data/app/services/collavre/comments/command_processor.rb +37 -0
  190. data/app/services/collavre/comments/mcp_command.rb +109 -0
  191. data/app/services/collavre/comments/mcp_command_builder.rb +32 -0
  192. data/app/services/collavre/creatives/filter_pipeline.rb +196 -0
  193. data/app/services/collavre/creatives/filters/assignee_filter.rb +30 -0
  194. data/app/services/collavre/creatives/filters/base_filter.rb +24 -0
  195. data/app/services/collavre/creatives/filters/comment_filter.rb +21 -0
  196. data/app/services/collavre/creatives/filters/date_filter.rb +58 -0
  197. data/app/services/collavre/creatives/filters/progress_filter.rb +25 -0
  198. data/app/services/collavre/creatives/filters/search_filter.rb +28 -0
  199. data/app/services/collavre/creatives/filters/tag_filter.rb +16 -0
  200. data/app/services/collavre/creatives/importer.rb +47 -0
  201. data/app/services/collavre/creatives/index_query.rb +191 -0
  202. data/app/services/collavre/creatives/path_exporter.rb +131 -0
  203. data/app/services/collavre/creatives/permission_cache_builder.rb +194 -0
  204. data/app/services/collavre/creatives/permission_checker.rb +42 -0
  205. data/app/services/collavre/creatives/plan_tagger.rb +53 -0
  206. data/app/services/collavre/creatives/progress_service.rb +89 -0
  207. data/app/services/collavre/creatives/reorderer.rb +187 -0
  208. data/app/services/collavre/creatives/tree_builder.rb +231 -0
  209. data/app/services/collavre/creatives/tree_formatter.rb +36 -0
  210. data/app/services/collavre/gemini_parent_recommender.rb +77 -0
  211. data/app/services/collavre/github/client.rb +112 -0
  212. data/app/services/collavre/github/pull_request_analyzer.rb +280 -0
  213. data/app/services/collavre/github/pull_request_processor.rb +181 -0
  214. data/app/services/collavre/github/webhook_provisioner.rb +130 -0
  215. data/app/services/collavre/google_calendar_service.rb +149 -0
  216. data/app/services/collavre/link_preview_fetcher.rb +230 -0
  217. data/app/services/collavre/markdown_importer.rb +202 -0
  218. data/app/services/collavre/mcp_service.rb +217 -0
  219. data/app/services/collavre/notion_client.rb +231 -0
  220. data/app/services/collavre/notion_creative_exporter.rb +296 -0
  221. data/app/services/collavre/notion_service.rb +249 -0
  222. data/app/services/collavre/ppt_importer.rb +76 -0
  223. data/app/services/collavre/ruby_llm_interaction_logger.rb +34 -0
  224. data/app/services/collavre/system_events/context_builder.rb +41 -0
  225. data/app/services/collavre/system_events/dispatcher.rb +19 -0
  226. data/app/services/collavre/system_events/router.rb +72 -0
  227. data/app/services/collavre/tools/creative_retrieval_service.rb +138 -0
  228. data/app/views/admin/shared/_tabs.html.erb +4 -0
  229. data/app/views/collavre/comments/_activity_log_details.html.erb +23 -0
  230. data/app/views/collavre/comments/_comment.html.erb +147 -0
  231. data/app/views/collavre/comments/_comments_popup.html.erb +46 -0
  232. data/app/views/collavre/comments/_list.html.erb +10 -0
  233. data/app/views/collavre/comments/_presence_avatars.html.erb +8 -0
  234. data/app/views/collavre/comments/_reaction_picker.html.erb +15 -0
  235. data/app/views/collavre/comments/_read_receipts.html.erb +19 -0
  236. data/app/views/collavre/creatives/_add_button.html.erb +20 -0
  237. data/app/views/collavre/creatives/_delete_button.html.erb +12 -0
  238. data/app/views/collavre/creatives/_github_integration_modal.html.erb +77 -0
  239. data/app/views/collavre/creatives/_import_upload_zone.html.erb +10 -0
  240. data/app/views/collavre/creatives/_inline_edit_form.html.erb +89 -0
  241. data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +24 -0
  242. data/app/views/collavre/creatives/_notion_integration_modal.html.erb +90 -0
  243. data/app/views/collavre/creatives/_set_plan_modal.html.erb +25 -0
  244. data/app/views/collavre/creatives/_share_button.html.erb +55 -0
  245. data/app/views/collavre/creatives/edit.html.erb +4 -0
  246. data/app/views/collavre/creatives/index.html.erb +192 -0
  247. data/app/views/collavre/creatives/new.html.erb +4 -0
  248. data/app/views/collavre/creatives/show.html.erb +29 -0
  249. data/app/views/collavre/creatives/slide_view.html.erb +20 -0
  250. data/app/views/collavre/email_verification_mailer/verify.html.erb +4 -0
  251. data/app/views/collavre/email_verification_mailer/verify.text.erb +2 -0
  252. data/app/views/collavre/emails/index.html.erb +19 -0
  253. data/app/views/collavre/emails/show.html.erb +5 -0
  254. data/app/views/collavre/inbox_items/_item.html.erb +18 -0
  255. data/app/views/collavre/inbox_items/_items.html.erb +3 -0
  256. data/app/views/collavre/inbox_items/_list.html.erb +7 -0
  257. data/app/views/collavre/inbox_items/index.html.erb +1 -0
  258. data/app/views/collavre/inbox_mailer/daily_summary.html.erb +11 -0
  259. data/app/views/collavre/inbox_mailer/daily_summary.text.erb +8 -0
  260. data/app/views/collavre/invitation_mailer/invite.html.erb +5 -0
  261. data/app/views/collavre/invitation_mailer/invite.text.erb +3 -0
  262. data/app/views/collavre/invites/show.html.erb +8 -0
  263. data/app/views/collavre/passwords/edit.html.erb +12 -0
  264. data/app/views/collavre/passwords/new.html.erb +17 -0
  265. data/app/views/collavre/passwords_mailer/reset.html.erb +4 -0
  266. data/app/views/collavre/passwords_mailer/reset.text.erb +2 -0
  267. data/app/views/collavre/sessions/new.html.erb +27 -0
  268. data/app/views/collavre/sessions/providers/_google.html.erb +7 -0
  269. data/app/views/collavre/sessions/providers/_passkey.html.erb +3 -0
  270. data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -0
  271. data/app/views/collavre/shared/_navigation.html.erb +37 -0
  272. data/app/views/collavre/shared/navigation/_help_button.html.erb +1 -0
  273. data/app/views/collavre/shared/navigation/_inbox_button.html.erb +5 -0
  274. data/app/views/collavre/shared/navigation/_mobile_inbox_button.html.erb +3 -0
  275. data/app/views/collavre/shared/navigation/_mobile_plans_button.html.erb +1 -0
  276. data/app/views/collavre/shared/navigation/_panels.html.erb +18 -0
  277. data/app/views/collavre/shared/navigation/_plans_button.html.erb +3 -0
  278. data/app/views/collavre/shared/navigation/_search_form.html.erb +6 -0
  279. data/app/views/collavre/user_themes/index.html.erb +75 -0
  280. data/app/views/collavre/users/_app_header.html.erb +5 -0
  281. data/app/views/collavre/users/_contact_management.html.erb +77 -0
  282. data/app/views/collavre/users/_id_pwd_fields.html.erb +27 -0
  283. data/app/views/collavre/users/edit_ai.html.erb +79 -0
  284. data/app/views/collavre/users/edit_password.html.erb +27 -0
  285. data/app/views/collavre/users/index.html.erb +80 -0
  286. data/app/views/collavre/users/new.html.erb +33 -0
  287. data/app/views/collavre/users/new_ai.html.erb +87 -0
  288. data/app/views/collavre/users/passkeys.html.erb +43 -0
  289. data/app/views/collavre/users/show.html.erb +143 -0
  290. data/app/views/inbox/badge_component/_count.html.erb +2 -0
  291. data/app/views/layouts/collavre/slide.html.erb +29 -0
  292. data/config/locales/comments.en.yml +114 -0
  293. data/config/locales/comments.ko.yml +110 -0
  294. data/config/locales/contacts.en.yml +16 -0
  295. data/config/locales/contacts.ko.yml +16 -0
  296. data/config/locales/creatives.en.yml +183 -0
  297. data/config/locales/creatives.ko.yml +164 -0
  298. data/config/locales/invites.en.yml +19 -0
  299. data/config/locales/invites.ko.yml +17 -0
  300. data/config/locales/notifications.en.yml +8 -0
  301. data/config/locales/notifications.ko.yml +8 -0
  302. data/config/locales/plans.en.yml +12 -0
  303. data/config/locales/plans.ko.yml +19 -0
  304. data/config/locales/themes.en.yml +29 -0
  305. data/config/locales/themes.ko.yml +27 -0
  306. data/config/locales/users.en.yml +151 -0
  307. data/config/locales/users.ko.yml +146 -0
  308. data/config/routes.rb +92 -0
  309. data/db/migrate/20241201000000_create_notion_integrations.rb +29 -0
  310. data/db/migrate/20250128110017_create_creatives.rb +11 -0
  311. data/db/migrate/20250128120122_create_users.rb +11 -0
  312. data/db/migrate/20250128120123_create_sessions.rb +11 -0
  313. data/db/migrate/20250128123633_create_subscribers.rb +10 -0
  314. data/db/migrate/20250312000000_create_notion_block_links.rb +16 -0
  315. data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +5 -0
  316. data/db/migrate/20250522115048_remove_name_from_creatives.rb +5 -0
  317. data/db/migrate/20250522190651_add_parent_id_to_creatives.rb +5 -0
  318. data/db/migrate/20250523133100_rename_inventory_count_to_progress.rb +13 -0
  319. data/db/migrate/20250523133101_add_sequence_to_creatives.rb +5 -0
  320. data/db/migrate/20250525205100_add_user_id_to_creatives.rb +10 -0
  321. data/db/migrate/20250527014217_create_creative_shares.rb +12 -0
  322. data/db/migrate/20250528142349_add_origin_id_to_creatives.rb +5 -0
  323. data/db/migrate/20250530060200_create_tags.rb +9 -0
  324. data/db/migrate/20250531105150_add_value_to_tags.rb +5 -0
  325. data/db/migrate/20250531140142_create_labels.rb +12 -0
  326. data/db/migrate/20250531140145_change_tag_to_label_reference.rb +6 -0
  327. data/db/migrate/20250601000000_create_comments.rb +10 -0
  328. data/db/migrate/20250601061830_drop_plans_and_variations.rb +6 -0
  329. data/db/migrate/20250604122600_add_owner_to_labels.rb +5 -0
  330. data/db/migrate/20250606000000_rename_email_address_to_email_in_users.rb +7 -0
  331. data/db/migrate/20250606150329_create_creative_hierarchies.rb +16 -0
  332. data/db/migrate/20250610142000_create_creative_expanded_states.rb +11 -0
  333. data/db/migrate/20250611030138_create_invitations.rb +15 -0
  334. data/db/migrate/20250611105524_create_inbox_items.rb +14 -0
  335. data/db/migrate/20250612150000_update_permissions.rb +43 -0
  336. data/db/migrate/20250612232913_add_email_verified_at_to_users.rb +5 -0
  337. data/db/migrate/20250616065905_add_avatar_to_users.rb +5 -0
  338. data/db/migrate/20250617092111_add_display_level_to_users.rb +5 -0
  339. data/db/migrate/20250620004558_create_emails.rb +13 -0
  340. data/db/migrate/20250621000000_create_comment_read_pointers.rb +11 -0
  341. data/db/migrate/20250622000000_add_completion_mark_to_users.rb +5 -0
  342. data/db/migrate/20250623000000_add_theme_to_users.rb +5 -0
  343. data/db/migrate/20250624000000_add_name_to_users.rb +18 -0
  344. data/db/migrate/20250714190000_create_devices.rb +17 -0
  345. data/db/migrate/20250715120000_add_notifications_enabled_to_users.rb +5 -0
  346. data/db/migrate/20250823000000_add_no_access_permission.rb +25 -0
  347. data/db/migrate/20250826000000_add_calendar_id_to_users.rb +5 -0
  348. data/db/migrate/20250827000000_add_google_oauth_tokens_to_users.rb +8 -0
  349. data/db/migrate/20250827061238_add_timezone_to_users.rb +5 -0
  350. data/db/migrate/20250828000000_create_calendar_events.rb +16 -0
  351. data/db/migrate/20250828060000_add_creative_to_calendar_events.rb +5 -0
  352. data/db/migrate/20250830141052_add_locale_to_users.rb +5 -0
  353. data/db/migrate/20250830141101_add_message_key_to_inbox_items.rb +6 -0
  354. data/db/migrate/20250902025423_remove_description_and_featured_image_from_creatives.rb +6 -0
  355. data/db/migrate/20250910000000_add_private_to_comments.rb +5 -0
  356. data/db/migrate/20250910105640_migrate_write_permissions_to_admin.rb +17 -0
  357. data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +27 -0
  358. data/db/migrate/20250923002959_deduplicate_device_fcm_tokens.rb +25 -0
  359. data/db/migrate/20250924000000_add_system_admin_to_users.rb +6 -0
  360. data/db/migrate/20250925000000_create_github_integrations.rb +26 -0
  361. data/db/migrate/20250927000000_add_webhook_secret_to_github_repository_links.rb +29 -0
  362. data/db/migrate/20250928000000_add_action_and_approver_to_comments.rb +6 -0
  363. data/db/migrate/20250928010000_add_action_execution_tracking_to_comments.rb +6 -0
  364. data/db/migrate/20250928105957_add_github_gemini_prompt_to_creatives.rb +5 -0
  365. data/db/migrate/20250929000000_add_comment_and_creative_refs_to_inbox_items.rb +6 -0
  366. data/db/migrate/20251001000001_create_contacts.rb +71 -0
  367. data/db/migrate/20251002000000_add_shared_by_to_creative_shares.rb +14 -0
  368. data/db/migrate/20251124120902_add_ai_fields_to_users.rb +7 -0
  369. data/db/migrate/20251124122218_add_created_by_id_to_users.rb +5 -0
  370. data/db/migrate/20251124124521_add_llm_api_key_to_users.rb +5 -0
  371. data/db/migrate/20251124130000_add_searchable_to_users.rb +6 -0
  372. data/db/migrate/20251125072705_migrate_linked_creative_children_to_origin.rb +28 -0
  373. data/db/migrate/20251126040752_add_description_to_creatives.rb +75 -0
  374. data/db/migrate/20251127000000_move_comments_from_linked_creatives_to_origins.rb +18 -0
  375. data/db/migrate/20251201073823_add_tools_to_users.rb +5 -0
  376. data/db/migrate/20251202062715_create_mcp_tools.rb +16 -0
  377. data/db/migrate/20251204125754_create_tasks_and_task_actions.rb +23 -0
  378. data/db/migrate/20251204125756_add_routing_expression_to_users.rb +5 -0
  379. data/db/migrate/20251204161133_set_default_routing_expression_for_ai_agents.rb +13 -0
  380. data/db/migrate/20251211033025_nullify_self_referencing_origins.rb +9 -0
  381. data/db/migrate/20251211080040_create_topics_and_add_topic_to_comments.rb +15 -0
  382. data/db/migrate/20251215143500_create_activity_logs.rb +16 -0
  383. data/db/migrate/20251222125727_create_webauthn_credentials.rb +14 -0
  384. data/db/migrate/20251222125839_add_webauthn_id_to_users.rb +5 -0
  385. data/db/migrate/20251223022315_create_comment_reactions.rb +13 -0
  386. data/db/migrate/20251223072625_create_user_themes.rb +11 -0
  387. data/db/migrate/20251230074456_add_creative_id_to_labels.rb +5 -0
  388. data/db/migrate/20251230113607_refactor_labels.rb +38 -0
  389. data/db/migrate/20251231010012_backfill_tags_for_labels.rb +15 -0
  390. data/db/migrate/20251231013234_drop_subscribers.rb +9 -0
  391. data/db/migrate/20260106090544_allow_null_user_id_in_creative_shares.rb +6 -0
  392. data/db/migrate/20260106160643_create_system_settings.rb +11 -0
  393. data/db/migrate/20260116000000_create_creative_shares_cache.rb +15 -0
  394. data/db/migrate/20260116000001_populate_creative_shares_cache.rb +23 -0
  395. data/db/migrate/20260119022933_make_source_share_id_nullable_in_creative_shares_caches.rb +7 -0
  396. data/db/migrate/20260119023446_populate_owner_cache_entries.rb +25 -0
  397. data/db/migrate/20260119100000_add_account_lockout_to_users.rb +6 -0
  398. data/db/migrate/20260119110000_add_last_active_at_to_sessions.rb +6 -0
  399. data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +60 -0
  400. data/db/migrate/20260120162259_remove_fk_from_creative_shares_caches.rb +7 -0
  401. data/db/migrate/20260120163856_remove_timestamps_from_creative_shares_caches.rb +6 -0
  402. data/lib/collavre/configuration.rb +14 -0
  403. data/lib/collavre/engine.rb +77 -0
  404. data/lib/collavre/user_extensions.rb +29 -0
  405. data/lib/collavre/version.rb +3 -0
  406. data/lib/collavre.rb +26 -0
  407. data/lib/generators/collavre/install/install_generator.rb +105 -0
  408. data/lib/generators/collavre/install/templates/build.cjs.tt +100 -0
  409. data/lib/tasks/collavre_assets.rake +15 -0
  410. 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