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,535 @@
1
+ module Collavre
2
+ class CreativesController < ApplicationController
3
+ # TODO: for not for security reasons for this Collavre app, we don't expose to public, later it should be controlled by roles for each Creatives
4
+ # Removed unauthenticated access to index and show actions
5
+ allow_unauthenticated_access only: %i[ index children export_markdown show slide_view ]
6
+ before_action :enforce_creatives_login_policy, only: %i[ index children export_markdown show slide_view ]
7
+ before_action :set_creative, only: %i[ show edit update destroy request_permission parent_suggestions slide_view unconvert ]
8
+
9
+ def index
10
+ respond_to do |format|
11
+ format.html do
12
+ # HTML only needs parent_creative for nav/title - skip expensive filtered queries
13
+ # Must check permission to avoid leaking metadata (og:title, etc.) to unauthorized users
14
+ if params[:id].present?
15
+ creative = Creative.find_by(id: params[:id])
16
+ @parent_creative = creative if creative&.has_permission?(Current.user, :read)
17
+ end
18
+ @creatives = [] # CSR will fetch via JSON
19
+ @shared_list = @parent_creative ? @parent_creative.all_shared_users : []
20
+ end
21
+ format.json do
22
+ # Full query only for JSON requests
23
+ user_id_for_state = Current.user&.id
24
+ if user_id_for_state.nil? && params[:id].present?
25
+ # Public view: use owner's state
26
+ target_creative = Creative.find_by(id: params[:id])
27
+ user_id_for_state = target_creative&.effective_origin&.user_id
28
+ end
29
+
30
+ @expanded_state_map = if user_id_for_state
31
+ CreativeExpandedState.where(user_id: user_id_for_state, creative_id: params[:id]).first&.expanded_status || {}
32
+ else
33
+ {}
34
+ end
35
+ index_result = ::Creatives::IndexQuery.new(user: Current.user, params: params.to_unsafe_h).call
36
+ @creatives = index_result.creatives || []
37
+ @parent_creative = index_result.parent_creative
38
+ @shared_creative = index_result.shared_creative
39
+ @shared_list = index_result.shared_list
40
+ @overall_progress = index_result.overall_progress if any_filter_active?
41
+ @allowed_creative_ids = index_result.allowed_creative_ids
42
+ @progress_map = index_result.progress_map
43
+
44
+ # Set filtered_progress on parent creative if progress_map is available
45
+ if @parent_creative && @progress_map && @progress_map.key?(@parent_creative.id.to_s)
46
+ @parent_creative.filtered_progress = @progress_map[@parent_creative.id.to_s]
47
+ end
48
+
49
+ # Disable caching for filtered results to ensure fresh data
50
+ expires_now if any_filter_active?
51
+
52
+ if params[:simple].present?
53
+ render json: serialize_creatives(@creatives)
54
+ else
55
+ @creatives_tree_json = build_tree(
56
+ index_result.creatives,
57
+ params: params,
58
+ expanded_state_map: @expanded_state_map,
59
+ level: 1,
60
+ allowed_creative_ids: @allowed_creative_ids,
61
+ progress_map: @progress_map
62
+ )
63
+ render json: { creatives: @creatives_tree_json }
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ def show
70
+ unless @creative.has_permission?(Current.user, :read)
71
+ if Current.user
72
+ redirect_to creatives_path, alert: t("collavre.creatives.errors.no_permission")
73
+ else
74
+ request_authentication
75
+ end
76
+ return
77
+ end
78
+
79
+ respond_to do |format|
80
+ redirect_options = { id: @creative.id }
81
+ redirect_options[:comment_id] = params[:comment_id] if params[:comment_id].present?
82
+ format.html { redirect_to creatives_path(redirect_options) }
83
+ format.json do
84
+ # Use HTTP caching with ETag - must vary by user since response includes user-specific data
85
+ # ETag must also include prompt comment and children timestamps since those are in response
86
+ effective = @creative.effective_origin
87
+ cache_user = Current.user&.id || "anon"
88
+
89
+ # Include prompt comment timestamp (user-specific private comments starting with "> ")
90
+ # Note: LIKE '> %' uses index prefix scan if available; consider dedicated prompt flag if bottleneck
91
+ prompt_updated = if Current.user
92
+ @creative.comments
93
+ .where(private: true, user: Current.user)
94
+ .where("content LIKE ?", "> %")
95
+ .maximum(:updated_at)
96
+ end
97
+
98
+ # Get children stats in a single query, reuse for has_children
99
+ # Use separate Arel.sql args so pick returns an array; unscope order to avoid Postgres aggregate error
100
+ children_count, children_max_updated = @creative.children
101
+ .unscope(:order)
102
+ .pick(Arel.sql("COUNT(*)"), Arel.sql("MAX(updated_at)"))
103
+ children_count = children_count.to_i # Handle nil and string type-casting from adapters
104
+ children_key = "#{children_count}-#{children_max_updated&.to_i}"
105
+
106
+ last_modified = [
107
+ @creative.updated_at,
108
+ effective.updated_at,
109
+ prompt_updated
110
+ ].compact.max
111
+
112
+ etag = [
113
+ "creative",
114
+ @creative.cache_key_with_version,
115
+ effective.cache_key_with_version,
116
+ "user",
117
+ cache_user,
118
+ "prompt",
119
+ prompt_updated&.to_i,
120
+ "children",
121
+ children_key
122
+ ].join(":")
123
+
124
+ if stale?(etag: etag, last_modified: last_modified, public: false)
125
+ root = params[:root_id] ? Creative.find_by(id: params[:root_id]) : nil
126
+ depth = if root
127
+ (@creative.ancestors.count - root.ancestors.count) + 1
128
+ else
129
+ @creative.ancestors.count + 1
130
+ end
131
+ render json: {
132
+ id: @creative.id,
133
+ description: @creative.effective_description,
134
+ description_raw_html: @creative.description,
135
+ origin_id: @creative.origin_id,
136
+ parent_id: @creative.parent_id,
137
+ progress: @creative.progress,
138
+ progress_html: view_context.render_creative_progress(@creative),
139
+ depth: depth,
140
+ prompt: @creative.prompt_for(Current.user),
141
+ has_children: children_count > 0
142
+ }
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ def slide_view
149
+ unless @creative.has_permission?(Current.user, :read)
150
+ if Current.user
151
+ redirect_to creatives_path, alert: t("collavre.creatives.errors.no_permission")
152
+ else
153
+ request_authentication
154
+ end
155
+ return
156
+ end
157
+
158
+ @slide_ids = []
159
+ @root_depth = @creative.ancestors.count
160
+ build_slide_ids(@creative)
161
+ render layout: "collavre/slide"
162
+ end
163
+
164
+ def new
165
+ @creative = Creative.new
166
+ if params[:parent_id].present?
167
+ @parent_creative = Creative.find_by(id: params[:parent_id])
168
+ @creative.parent = @parent_creative if @parent_creative
169
+ end
170
+ if params[:child_id].present?
171
+ @child_creative = Creative.find_by(id: params[:child_id])
172
+ end
173
+ if params[:after_id].present?
174
+ @after_creative = Creative.find_by(id: params[:after_id])
175
+ end
176
+ end
177
+
178
+ def create
179
+ @creative = Creative.new(creative_params)
180
+ if @creative.parent
181
+ @creative.user = @creative.parent.user
182
+ else
183
+ @creative.user = Current.user
184
+ end
185
+
186
+ # Rebuild @child_creative from params if present
187
+ if params[:child_id].present?
188
+ @child_creative = Creative.find_by(id: params[:child_id])
189
+ end
190
+
191
+ if @creative.save
192
+ @child_creative.update(parent: @creative) if @child_creative
193
+ if params[:before_id].present?
194
+ before_creative = Creative.find_by(id: params[:before_id])
195
+ if before_creative && before_creative.parent_id == @creative.parent_id
196
+ siblings = @creative.parent ? @creative.parent.children.order(:sequence).to_a : Creative.roots.order(:sequence).to_a
197
+ siblings.reject! { |s| s.id == @creative.id }
198
+ index = siblings.index { |s| s.id == before_creative.id } || 0
199
+ siblings.insert(index, @creative)
200
+ siblings.each_with_index { |c, idx| c.update_column(:sequence, idx) }
201
+ end
202
+ elsif params[:after_id].present?
203
+ after_creative = Creative.find_by(id: params[:after_id])
204
+ if after_creative && after_creative.parent_id == @creative.parent_id
205
+ siblings = @creative.parent ? @creative.parent.children.order(:sequence).to_a : Creative.roots.order(:sequence).to_a
206
+ siblings.reject! { |s| s.id == @creative.id }
207
+ index = siblings.index { |s| s.id == after_creative.id } || -1
208
+ siblings.insert(index + 1, @creative)
209
+ siblings.each_with_index { |c, idx| c.update_column(:sequence, idx) }
210
+ end
211
+ end
212
+ if params[:tags].present?
213
+ Array(params[:tags]).each do |tag_id|
214
+ @creative.tags.create(label_id: tag_id)
215
+ end
216
+ end
217
+ render json: { id: @creative.id }
218
+ else
219
+ render json: { errors: @creative.errors.full_messages }, status: :unprocessable_entity
220
+ end
221
+ end
222
+
223
+ def parent_suggestions
224
+ suggestions = ::GeminiParentRecommender.new.recommend(@creative)
225
+ render json: suggestions
226
+ end
227
+
228
+ def edit
229
+ if params[:inline]
230
+ render partial: "inline_edit_form"
231
+ end
232
+ end
233
+
234
+ def update
235
+ respond_to do |format|
236
+ permitted = creative_params.to_h
237
+ base = @creative.effective_origin(Set.new)
238
+ success = true
239
+
240
+ # Handle parent_id change separately for Linked Creatives
241
+ if @creative.origin_id.present? && permitted.key?("parent_id")
242
+ parent_id = permitted.delete("parent_id")
243
+ success &&= @creative.update(parent_id: parent_id)
244
+ end
245
+
246
+ # When updating the base (Origin), we must NOT pass origin_id.
247
+ # Because if @creative is Linked, params might include origin_id.
248
+ # Passing origin_id to the Origin creative causes it to fail validation (cannot changes if has origin)
249
+ # or creates a self-cycle.
250
+ permitted.delete("origin_id")
251
+ permitted.delete(:origin_id)
252
+
253
+ success &&= base.update(permitted)
254
+
255
+ if success
256
+ format.html { redirect_to @creative }
257
+ format.json { head :ok }
258
+ else
259
+ format.html { render :edit, status: :unprocessable_entity }
260
+ format.json { render json: { errors: @creative.errors.full_messages }, status: :unprocessable_entity }
261
+ end
262
+ end
263
+ end
264
+
265
+ def destroy
266
+ parent = @creative.parent
267
+ unless @creative.has_permission?(Current.user, :admin)
268
+ redirect_to @creative, alert: t("collavre.creatives.errors.no_permission") and return
269
+ end
270
+ if params[:delete_with_children]
271
+ # Recursively destroy deletable descendants before deleting parent
272
+ destroy_descendants_recursively(@creative, Current.user)
273
+ else
274
+ # Re-link children to parent
275
+ @creative.children.each { |child| child.update(parent: parent) }
276
+ end
277
+ CreativeShare.where(creative: @creative).destroy_all
278
+ @creative.destroy
279
+ end
280
+
281
+ def request_permission
282
+ creative = @creative.effective_origin
283
+ if creative.user == Current.user || creative.has_permission?(Current.user, :read)
284
+ return head :unprocessable_entity
285
+ end
286
+
287
+ short_title = helpers.strip_tags(creative.effective_origin.description).truncate(10)
288
+
289
+ InboxItem.create!(
290
+ owner: creative.user,
291
+ message_key: "inbox.permission_requested",
292
+ message_params: { user: Current.user.display_name, short_title: short_title },
293
+ link: creative_url(
294
+ creative,
295
+ Rails.application.config.action_mailer.default_url_options.merge(share_request: Current.user.email)
296
+ )
297
+ )
298
+
299
+ head :ok
300
+ end
301
+
302
+
303
+
304
+ def reorder
305
+ dragged_ids = Array(params[:dragged_ids]).map(&:presence).compact
306
+ target_id = params[:target_id]
307
+ direction = params[:direction]
308
+
309
+ if dragged_ids.any?
310
+ reorderer.reorder_multiple(
311
+ dragged_ids: dragged_ids,
312
+ target_id: target_id,
313
+ direction: direction
314
+ )
315
+ else
316
+ reorderer.reorder(
317
+ dragged_id: params[:dragged_id],
318
+ target_id: target_id,
319
+ direction: direction
320
+ )
321
+ end
322
+ head :ok
323
+ rescue ::Creatives::Reorderer::Error
324
+ head :unprocessable_entity
325
+ end
326
+
327
+ def link_drop
328
+ result = reorderer.link_drop(
329
+ dragged_id: params[:dragged_id],
330
+ target_id: params[:target_id],
331
+ direction: params[:direction]
332
+ )
333
+
334
+ new_creative = result.new_creative
335
+ level = new_creative.ancestors.count + 1
336
+ nodes = build_tree(
337
+ [ new_creative ],
338
+ params: params,
339
+ expanded_state_map: {},
340
+ level: level
341
+ )
342
+
343
+ render json: {
344
+ nodes: nodes,
345
+ creative_id: new_creative.id,
346
+ parent_id: result.parent&.id,
347
+ direction: result.direction
348
+ }
349
+ rescue ::Creatives::Reorderer::Error
350
+ head :unprocessable_entity
351
+ end
352
+
353
+ def append_as_parent
354
+ @parent_creative = Creative.find_by(id: params[:parent_id]).parent
355
+ redirect_to new_creative_path(parent_id: @parent_creative&.id, child_id: params[:parent_id], tags: params[:tags])
356
+ end
357
+
358
+ def append_below
359
+ target = Creative.find_by(id: params[:creative_id])
360
+ redirect_to new_creative_path(parent_id: target&.parent_id, after_id: target&.id, tags: params[:tags])
361
+ end
362
+
363
+ def children
364
+ parent = Creative.find(params[:id])
365
+ effective = parent.effective_origin
366
+ # user_id for expanded_state lookup - use owner's state for anonymous users
367
+ state_user_id = Current.user&.id || effective.user_id
368
+
369
+ # HTTP caching disabled for children endpoint:
370
+ # Response depends on child updates, permission changes (CreativeSharesCache),
371
+ # and CreativeExpandedState. Tracking all dependencies reliably is expensive
372
+ # (requires descendant_ids query). Stale 304 responses could leak data after
373
+ # permission revocation. Re-enable when a cheap version key mechanism exists.
374
+ # Use private + no-store to prevent any caching (proxy or browser).
375
+ response.headers["Cache-Control"] = "private, no-store"
376
+
377
+ has_filters = params[:tags].present? || params[:min_progress].present? || params[:max_progress].present?
378
+ if has_filters
379
+ result = ::Creatives::IndexQuery.new(user: Current.user, params: params.merge(id: params[:id])).call
380
+ render_children_json(parent, state_user_id, result.allowed_creative_ids, result.progress_map)
381
+ else
382
+ render_children_json(parent, state_user_id, nil, nil)
383
+ end
384
+ end
385
+
386
+ def unconvert
387
+ base_creative = @creative.effective_origin
388
+ parent = base_creative.parent
389
+ if parent.nil?
390
+ render json: { error: t("collavre.creatives.index.unconvert_no_parent") }, status: :unprocessable_entity and return
391
+ end
392
+
393
+ unless parent.has_permission?(Current.user, :feedback)
394
+ render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
395
+ end
396
+
397
+ unless base_creative.has_permission?(Current.user, :admin)
398
+ render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
399
+ end
400
+
401
+ markdown = helpers.render_creative_tree_markdown([ base_creative ])
402
+ comment = nil
403
+
404
+ ActiveRecord::Base.transaction do
405
+ comment = parent.effective_origin.comments.create!(content: markdown, user: Current.user)
406
+ base_creative.descendants.each(&:destroy!)
407
+ base_creative.destroy!
408
+ end
409
+
410
+ render json: { comment_id: comment.id }, status: :created
411
+ rescue ActiveRecord::RecordInvalid => e
412
+ render json: { error: e.record.errors.full_messages.to_sentence }, status: :unprocessable_entity
413
+ end
414
+
415
+ def export_markdown
416
+ creatives = if params[:parent_id]
417
+ parent_creative = Creative.find(params[:parent_id])
418
+ effective_origin = parent_creative.effective_origin
419
+ unless parent_creative.has_permission?(Current.user, :read) &&
420
+ effective_origin.has_permission?(Current.user, :read)
421
+ render plain: t("collavre.creatives.errors.no_permission"), status: :forbidden and return
422
+ end
423
+ [ effective_origin ]
424
+ else
425
+ Creative.where(parent_id: nil).map(&:effective_origin).uniq.select do |creative|
426
+ creative.has_permission?(Current.user, :read)
427
+ end
428
+ end
429
+
430
+ if creatives.empty?
431
+ render plain: t("collavre.creatives.errors.no_permission"), status: :forbidden and return
432
+ end
433
+
434
+ markdown = helpers.render_creative_tree_markdown(creatives)
435
+ send_data markdown, filename: "creatives.md", type: "text/markdown"
436
+ end
437
+
438
+ private
439
+ def build_tree(collection, params:, expanded_state_map:, level:, select_mode: false, allowed_creative_ids: nil, progress_map: nil)
440
+ ::Creatives::TreeBuilder.new(
441
+ user: Current.user,
442
+ params: params,
443
+ view_context: view_context,
444
+ expanded_state_map: expanded_state_map,
445
+ select_mode: select_mode,
446
+ max_level: Current.user&.display_level || User::DEFAULT_DISPLAY_LEVEL,
447
+ allowed_creative_ids: allowed_creative_ids,
448
+ progress_map: progress_map
449
+ ).build(collection, level: level)
450
+ end
451
+
452
+ def set_creative
453
+ @creative = Creative.find(params[:id])
454
+ end
455
+
456
+ def creative_params
457
+ params.require(:creative).permit(:description, :progress, :parent_id, :sequence, :origin_id)
458
+ end
459
+
460
+ def any_filter_active?
461
+ params[:tags].present? ||
462
+ params[:min_progress].present? ||
463
+ params[:max_progress].present? ||
464
+ params[:search].present? ||
465
+ params[:comment] == "true" ||
466
+ params[:has_comments].present? ||
467
+ params[:due_before].present? ||
468
+ params[:due_after].present? ||
469
+ params[:has_due_date].present? ||
470
+ params[:assignee_id].present? ||
471
+ params[:unassigned].present?
472
+ end
473
+
474
+ def build_slide_ids(node)
475
+ return unless node.has_permission?(Current.user, :read)
476
+
477
+ @slide_ids << node.id
478
+ children = node.children.order(:sequence)
479
+ if node.origin_id.present?
480
+ linked_children = node.linked_children
481
+ children = (children + linked_children).uniq.sort_by(&:sequence)
482
+ end
483
+ children.each { |child| build_slide_ids(child) }
484
+ end
485
+
486
+ def serialize_creatives(collection)
487
+ if params[:simple].present?
488
+ collection.map { |c| { id: c.id, description: c.effective_description(nil, false), progress: c.progress } }
489
+ else
490
+ collection.map { |c| { id: c.id, description: c.effective_description, progress: c.progress } }
491
+ end
492
+ end
493
+
494
+ def reorderer
495
+ @reorderer ||= ::Creatives::Reorderer.new(user: Current.user)
496
+ end
497
+
498
+ def render_children_json(parent, user_id, allowed_ids, progress_map)
499
+ expanded_state_map = CreativeExpandedState
500
+ .where(user_id: user_id, creative_id: parent.id)
501
+ .first&.expanded_status || {}
502
+ children = parent.children_with_permission(Current.user)
503
+
504
+ level = params[:level].to_i
505
+ json_level = level.zero? ? 1 : level
506
+ render json: {
507
+ creatives: build_tree(
508
+ children,
509
+ params: params,
510
+ expanded_state_map: expanded_state_map,
511
+ level: json_level,
512
+ select_mode: params[:select_mode] == "1",
513
+ allowed_creative_ids: allowed_ids,
514
+ progress_map: progress_map
515
+ )
516
+ }
517
+ end
518
+
519
+ # Recursively destroy all descendants the user can delete
520
+ def destroy_descendants_recursively(creative, user)
521
+ deletable_children = creative.children_with_permission(user, :admin)
522
+ deletable_children.each do |child|
523
+ destroy_descendants_recursively(child, user)
524
+ CreativeShare.where(creative: child).destroy_all
525
+ child.destroy
526
+ end
527
+ end
528
+
529
+ def enforce_creatives_login_policy
530
+ if SystemSetting.creatives_login_required?
531
+ require_authentication
532
+ end
533
+ end
534
+ end
535
+ end
@@ -0,0 +1,19 @@
1
+ module Collavre
2
+ class DevicesController < ApplicationController
3
+ def create
4
+ device = Device.find_by(fcm_token: device_params[:fcm_token]) ||
5
+ Current.user.devices.find_or_initialize_by(client_id: device_params[:client_id])
6
+
7
+ device.assign_attributes(device_params)
8
+ device.user = Current.user
9
+ device.save!
10
+ head :no_content
11
+ end
12
+
13
+ private
14
+
15
+ def device_params
16
+ params.require(:device).permit(:client_id, :device_type, :app_id, :app_version, :fcm_token)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module Collavre
2
+ class EmailVerificationsController < ApplicationController
3
+ allow_unauthenticated_access
4
+
5
+ def show
6
+ user = Collavre::User.find_by_token_for(:email_verification, params[:token])
7
+
8
+ if user
9
+ user.update!(email_verified_at: Time.current)
10
+ redirect_to new_session_path, notice: I18n.t("collavre.users.email_verified")
11
+ else
12
+ redirect_to new_session_path, alert: I18n.t("collavre.users.email_verification.invalid_token")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Collavre
2
+ class EmailsController < ApplicationController
3
+ def index
4
+ @emails = Email.order(created_at: :desc).limit(50)
5
+ end
6
+
7
+ def show
8
+ @email = Email.find(params[:id])
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ module Collavre
2
+ class GithubAuthController < ApplicationController
3
+ allow_unauthenticated_access only: :callback
4
+ before_action -> { enforce_auth_provider!(:github) }, only: :callback
5
+
6
+ def callback
7
+ auth = request.env["omniauth.auth"]
8
+ gh = Collavre::GithubAccount.find_or_initialize_by(github_uid: auth.uid)
9
+
10
+ if gh.new_record?
11
+ unless Current.user
12
+ redirect_to collavre.new_session_path, alert: I18n.t("collavre.github_auth.login_first")
13
+ return
14
+ end
15
+ gh.user = Current.user
16
+ end
17
+
18
+ gh.token = auth.credentials.token
19
+ gh.login = auth.info.nickname
20
+ gh.save!
21
+
22
+ redirect_to creatives_path, notice: I18n.t("collavre.github_auth.connected")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ module Collavre
2
+ class GoogleAuthController < ApplicationController
3
+ allow_unauthenticated_access only: :callback
4
+ before_action -> { enforce_auth_provider!(:google) }, only: :callback
5
+
6
+ def callback
7
+ auth = request.env["omniauth.auth"]
8
+ user = Collavre::User.find_or_initialize_by(email: auth.info.email)
9
+ if user.new_record?
10
+ user.name = auth.info.name.presence || auth.info.email.split("@").first
11
+ random_password = SecureRandom.hex(16)
12
+ user.password = random_password
13
+ user.password_confirmation = random_password
14
+ user.email_verified_at = Time.current
15
+ end
16
+
17
+ if auth.info.image.present? && !user.avatar.attached? && user.avatar_url.blank?
18
+ user.avatar_url = auth.info.image
19
+ end
20
+
21
+ # for personal google service (like google calendar)
22
+ user.google_uid = auth.uid
23
+ user.google_access_token = auth.credentials.token
24
+ user.google_refresh_token = auth.credentials.refresh_token || user.google_refresh_token
25
+ user.google_token_expires_at = Time.at(auth.credentials.expires_at) if auth.credentials.expires_at
26
+
27
+ user.save! if user.new_record? || user.changed?
28
+
29
+ # Ensure app calendar exists if the granted scope allows creating an app calendar
30
+ begin
31
+ ::GoogleCalendarService.new(user: user).ensure_app_calendar!
32
+ rescue => e
33
+ Rails.logger.error("Post-login calendar setup failed: #{e.message}")
34
+ end
35
+
36
+ tz = request.env["omniauth.params"] && request.env["omniauth.params"]["timezone"]
37
+ user.update(timezone: tz) if tz.present? && user.timezone != tz
38
+
39
+ start_new_session_for(user)
40
+ redirect_to after_authentication_url
41
+ end
42
+ end
43
+ end