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,846 @@
1
+ import {
2
+ DRAGGABLE_SELECTOR,
3
+ clearDragHighlight,
4
+ asTreeRow,
5
+ getChildrenContainer,
6
+ ensureChildrenContainer,
7
+ appendBlockToContainer,
8
+ moveBlockBefore,
9
+ moveBlockAfter,
10
+ isDescendantRow,
11
+ applyLevelDelta,
12
+ setRowParent,
13
+ setRowRootState,
14
+ setHasChildren,
15
+ setExpanded,
16
+ syncParentHasChildren,
17
+ } from './dom';
18
+ import {
19
+ setDraggedState,
20
+ getDraggedState,
21
+ resetDraggedState,
22
+ setLastDragOverRow,
23
+ getLastDragOverRow,
24
+ hasDraggedState,
25
+ } from './state';
26
+ import { createMoveContext, applyMove, revertMove } from './operations';
27
+ import { sendNewOrder, sendLinkedCreative } from '../../lib/api/drag_drop';
28
+ import { initIndicator, showLinkHover, hideLinkHover } from './indicator';
29
+
30
+ const childZoneRatio = 0.3;
31
+ const coordPrecision = 5;
32
+
33
+ const TRANSFER_MIME_TYPE = 'application/x-plan42-creative';
34
+ const DRAG_TOKEN_STORAGE_KEY = 'plan42.dragToken';
35
+ const DROP_SIGNAL_STORAGE_KEY = 'plan42.dragDropSignal';
36
+ const WINDOW_ID_SESSION_KEY = 'plan42.dragWindowId';
37
+ const INVALID_DROP_MESSAGE =
38
+ 'We could not verify that drop. Please refresh the page and try again.';
39
+ const DROP_COMPLETED_EVENT = 'plan42:creative-drop-complete';
40
+
41
+ let cachedDragToken;
42
+ let cachedWindowId;
43
+
44
+ function getRowByCreativeId(creativeId) {
45
+ if (typeof document === 'undefined' || !creativeId) return null;
46
+ return document.querySelector(`creative-tree-row[creative-id="${creativeId}"]`);
47
+ }
48
+
49
+ function generateRandomIdentifier(context) {
50
+ const fallback = () =>
51
+ `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
52
+
53
+ if (typeof window === 'undefined') return fallback();
54
+
55
+ try {
56
+ const { crypto } = window;
57
+ if (crypto && typeof crypto.randomUUID === 'function') {
58
+ return crypto.randomUUID();
59
+ }
60
+ } catch (error) {
61
+ if (context) {
62
+ console.error(`Failed to access crypto API for ${context}`, error);
63
+ } else {
64
+ console.error('Failed to access crypto API for identifier generation', error);
65
+ }
66
+ }
67
+
68
+ return fallback();
69
+ }
70
+
71
+ function generateDragToken() {
72
+ if (typeof window === 'undefined') return null;
73
+
74
+ return generateRandomIdentifier('drag token generation');
75
+ }
76
+
77
+ function collectSelectedCreativeIds(activeCreativeId) {
78
+ if (typeof document === 'undefined') {
79
+ return activeCreativeId ? [String(activeCreativeId)] : [];
80
+ }
81
+
82
+ const seen = new Set();
83
+ const ids = [];
84
+
85
+ const selectedCheckboxes = document.querySelectorAll('.select-creative-checkbox:checked');
86
+ selectedCheckboxes.forEach((checkbox) => {
87
+ const value = checkbox?.value;
88
+ if (!value) return;
89
+ const str = String(value);
90
+ if (seen.has(str)) return;
91
+ seen.add(str);
92
+ ids.push(str);
93
+ });
94
+
95
+ if (activeCreativeId) {
96
+ const str = String(activeCreativeId);
97
+ if (!seen.has(str)) {
98
+ ids.push(str);
99
+ }
100
+ }
101
+
102
+ return ids;
103
+ }
104
+
105
+ function resolveDraggedIds(state) {
106
+ if (!state) return [];
107
+
108
+ const list = Array.isArray(state.selectedCreativeIds)
109
+ ? state.selectedCreativeIds
110
+ : [];
111
+ const seen = new Set();
112
+ const result = [];
113
+
114
+ [...list, state.creativeId].forEach((id) => {
115
+ if (!id && id !== 0) return;
116
+ const str = String(id);
117
+ if (seen.has(str)) return;
118
+ seen.add(str);
119
+ result.push(str);
120
+ });
121
+
122
+ return result;
123
+ }
124
+
125
+ function readStoredDragToken() {
126
+ if (cachedDragToken) return cachedDragToken;
127
+ if (typeof window === 'undefined') return null;
128
+
129
+ try {
130
+ const storage = window.localStorage;
131
+ if (!storage) return null;
132
+
133
+ const storedToken = storage.getItem(DRAG_TOKEN_STORAGE_KEY);
134
+ if (storedToken) {
135
+ cachedDragToken = storedToken;
136
+ }
137
+
138
+ return cachedDragToken || null;
139
+ } catch (error) {
140
+ console.error('Failed to read drag session token from storage', error);
141
+ return null;
142
+ }
143
+ }
144
+
145
+ function ensureDragSessionToken() {
146
+ if (typeof window === 'undefined') return null;
147
+
148
+ const existing = readStoredDragToken();
149
+ if (existing) return existing;
150
+
151
+ try {
152
+ const storage = window.localStorage;
153
+ if (!storage) return null;
154
+
155
+ const freshToken = generateDragToken();
156
+ if (!freshToken) return null;
157
+
158
+ storage.setItem(DRAG_TOKEN_STORAGE_KEY, freshToken);
159
+ cachedDragToken = freshToken;
160
+ return freshToken;
161
+ } catch (error) {
162
+ console.error('Failed to persist drag session token', error);
163
+ return null;
164
+ }
165
+ }
166
+
167
+ function readWindowId() {
168
+ if (cachedWindowId) return cachedWindowId;
169
+ if (typeof window === 'undefined') return null;
170
+
171
+ try {
172
+ const storage = window.sessionStorage;
173
+ if (!storage) return null;
174
+
175
+ const stored = storage.getItem(WINDOW_ID_SESSION_KEY);
176
+ if (stored) {
177
+ cachedWindowId = stored;
178
+ }
179
+ return cachedWindowId || null;
180
+ } catch (error) {
181
+ console.error('Failed to read drag window id from session storage', error);
182
+ return cachedWindowId || null;
183
+ }
184
+ }
185
+
186
+ function ensureWindowId() {
187
+ if (typeof window === 'undefined') return null;
188
+
189
+ const existing = readWindowId();
190
+ if (existing) return existing;
191
+
192
+ const freshId = generateRandomIdentifier('drag window id generation');
193
+ if (!freshId) return null;
194
+
195
+ try {
196
+ const storage = window.sessionStorage;
197
+ storage?.setItem(WINDOW_ID_SESSION_KEY, freshId);
198
+ cachedWindowId = freshId;
199
+ return freshId;
200
+ } catch (error) {
201
+ console.error('Failed to persist drag window id', error);
202
+ cachedWindowId = freshId;
203
+ return freshId;
204
+ }
205
+ }
206
+
207
+ function resolveDraggedStateFromDom(state) {
208
+ if (!state) return null;
209
+ if (typeof document === 'undefined') return null;
210
+
211
+ const { creativeId, treeId = null } = state;
212
+ if (!creativeId) return null;
213
+
214
+ let tree = treeId ? document.getElementById(treeId) : null;
215
+ let row = tree ? asTreeRow(tree) : null;
216
+
217
+ if (!row) {
218
+ row = getRowByCreativeId(creativeId);
219
+ tree = row ? row.querySelector(DRAGGABLE_SELECTOR) : tree;
220
+ }
221
+
222
+ if (!row || !tree) return null;
223
+
224
+ const parentId = row.getAttribute('parent-id') || null;
225
+ const level = Number(row.getAttribute('level') || row.level || state.level || 1);
226
+ const isRoot = row.hasAttribute('is-root');
227
+
228
+ return {
229
+ ...state,
230
+ tree,
231
+ treeId: tree.id || state.treeId || null,
232
+ row,
233
+ parentId,
234
+ level,
235
+ isRoot,
236
+ };
237
+ }
238
+
239
+ function resolveTargetTree(targetTreeId) {
240
+ if (typeof document === 'undefined' || !targetTreeId) return null;
241
+ const tree = document.getElementById(targetTreeId);
242
+ if (!tree) return null;
243
+ const row = asTreeRow(tree);
244
+ if (!row) return null;
245
+ return { tree, row };
246
+ }
247
+
248
+ function relaxedCoord(value) {
249
+ return Math.round(value / coordPrecision) * coordPrecision;
250
+ }
251
+
252
+ function serializeDragState(state, sessionToken) {
253
+ if (!sessionToken) return null;
254
+
255
+ try {
256
+ return JSON.stringify({
257
+ creativeId: state.creativeId,
258
+ treeId: state.treeId,
259
+ parentId: state.parentId,
260
+ level: state.level,
261
+ isRoot: state.isRoot,
262
+ token: sessionToken,
263
+ sourceWindowId: state.sourceWindowId,
264
+ selectedCreativeIds: Array.isArray(state.selectedCreativeIds)
265
+ ? state.selectedCreativeIds
266
+ : [],
267
+ });
268
+ } catch (error) {
269
+ console.error('Failed to serialize drag state', error);
270
+ return null;
271
+ }
272
+ }
273
+
274
+ function parseDragState(data) {
275
+ if (!data) return null;
276
+
277
+ try {
278
+ const parsed = JSON.parse(data);
279
+ if (parsed && parsed.creativeId && parsed.treeId) {
280
+ const expectedToken = readStoredDragToken();
281
+ if (!expectedToken || parsed.token !== expectedToken) {
282
+ return null;
283
+ }
284
+
285
+ const {
286
+ creativeId,
287
+ treeId,
288
+ parentId = null,
289
+ level,
290
+ isRoot,
291
+ sourceWindowId = null,
292
+ selectedCreativeIds = [],
293
+ } = parsed;
294
+ const normalizedSelected = Array.isArray(selectedCreativeIds)
295
+ ? selectedCreativeIds.map((id) => String(id)).filter((value, index, array) => array.indexOf(value) === index)
296
+ : [];
297
+ return {
298
+ creativeId,
299
+ treeId,
300
+ parentId,
301
+ level,
302
+ isRoot,
303
+ sourceWindowId,
304
+ selectedCreativeIds: normalizedSelected,
305
+ };
306
+ }
307
+ } catch (error) {
308
+ console.error('Failed to parse drag data', error);
309
+ }
310
+
311
+ return null;
312
+ }
313
+
314
+ function emitDropSignal(detail) {
315
+ if (typeof window === 'undefined') return;
316
+
317
+ const sessionToken = readStoredDragToken();
318
+ if (!sessionToken) return;
319
+
320
+ try {
321
+ const storage = window.localStorage;
322
+ if (!storage) return;
323
+
324
+ const payload = JSON.stringify({
325
+ ...detail,
326
+ sessionToken,
327
+ nonce: generateRandomIdentifier('drag drop signal'),
328
+ });
329
+
330
+ storage.setItem(DROP_SIGNAL_STORAGE_KEY, payload);
331
+ storage.removeItem(DROP_SIGNAL_STORAGE_KEY);
332
+ } catch (error) {
333
+ console.error('Failed to broadcast drop completion signal', error);
334
+ }
335
+ }
336
+
337
+ function dispatchDropCompletion(detail) {
338
+ if (typeof window === 'undefined') return;
339
+
340
+ try {
341
+ window.dispatchEvent(
342
+ new CustomEvent(DROP_COMPLETED_EVENT, {
343
+ detail,
344
+ })
345
+ );
346
+ } catch (error) {
347
+ console.error('Failed to dispatch creative drop completion event', error);
348
+ }
349
+ }
350
+
351
+ function removeDroppedCreative({ creativeId, treeId }) {
352
+ if (typeof document === 'undefined') return;
353
+
354
+ const tree = treeId ? document.getElementById(treeId) : null;
355
+ const row = tree ? asTreeRow(tree) : null;
356
+ const fallbackRow = getRowByCreativeId(creativeId);
357
+ const targetRow = row || fallbackRow;
358
+ if (!targetRow) return;
359
+
360
+ const parentId = targetRow.getAttribute('parent-id') || null;
361
+ const childrenContainer = getChildrenContainer(targetRow);
362
+ if (childrenContainer) {
363
+ childrenContainer.remove();
364
+ }
365
+ targetRow.remove();
366
+
367
+ syncParentHasChildren(parentId);
368
+ }
369
+
370
+ function syncSourceWindowDrop(detail) {
371
+ const { creativeId, treeId = null, direction, targetTreeId = null } = detail;
372
+ if (!creativeId || !direction || !targetTreeId) {
373
+ removeDroppedCreative({ creativeId, treeId });
374
+ return;
375
+ }
376
+
377
+ const resolvedDraggedState = resolveDraggedStateFromDom({ creativeId, treeId });
378
+ if (!resolvedDraggedState) {
379
+ removeDroppedCreative({ creativeId, treeId });
380
+ return;
381
+ }
382
+
383
+ const target = resolveTargetTree(targetTreeId);
384
+ if (!target) {
385
+ removeDroppedCreative({ creativeId, treeId });
386
+ return;
387
+ }
388
+
389
+ if (isDescendantRow(resolvedDraggedState.row, target.row)) {
390
+ removeDroppedCreative({ creativeId, treeId });
391
+ return;
392
+ }
393
+
394
+ const draggedChildren = getChildrenContainer(resolvedDraggedState.row);
395
+ const moveContext = createMoveContext(
396
+ resolvedDraggedState,
397
+ target.row,
398
+ draggedChildren
399
+ );
400
+
401
+ let newParentId = resolvedDraggedState.parentId;
402
+
403
+ try {
404
+ ({ newParentId } = applyMove({
405
+ direction,
406
+ targetRow: target.row,
407
+ draggedState: resolvedDraggedState,
408
+ draggedChildren,
409
+ moveContext,
410
+ }));
411
+ } catch (error) {
412
+ console.error('Failed to synchronize drop in source window', error);
413
+ revertMove(moveContext, newParentId);
414
+ }
415
+ }
416
+
417
+ function handleStorageChange(event) {
418
+ if (!event || event.key !== DROP_SIGNAL_STORAGE_KEY || !event.newValue) {
419
+ return;
420
+ }
421
+
422
+ let payload;
423
+ try {
424
+ payload = JSON.parse(event.newValue);
425
+ } catch (error) {
426
+ console.error('Failed to parse drop completion payload', error);
427
+ return;
428
+ }
429
+
430
+ const expectedToken = readStoredDragToken();
431
+ if (!expectedToken || payload.sessionToken !== expectedToken) {
432
+ return;
433
+ }
434
+
435
+ const windowId = readWindowId();
436
+ if (!windowId || payload.sourceWindowId !== windowId) {
437
+ return;
438
+ }
439
+
440
+ const { creativeId } = payload;
441
+ if (!creativeId) return;
442
+
443
+ dispatchDropCompletion({
444
+ ...payload,
445
+ context: 'source',
446
+ });
447
+ }
448
+
449
+ function handleDropCompletionEvent(event) {
450
+ if (!event || !event.detail) return;
451
+
452
+ const detail = event.detail;
453
+ const { creativeId, treeId = null, sourceWindowId = null, context } = detail;
454
+ if (!creativeId || !context) return;
455
+
456
+ const windowId = readWindowId();
457
+ if (!windowId || sourceWindowId !== windowId) {
458
+ return;
459
+ }
460
+
461
+ if (context === 'source') {
462
+ syncSourceWindowDrop(detail);
463
+ }
464
+ }
465
+
466
+ function getDraggedContext(event) {
467
+ const existing = getDraggedState();
468
+ const transfer = event.dataTransfer;
469
+ const transferTypes = transfer?.types
470
+ ? new Set(Array.from(transfer.types))
471
+ : new Set();
472
+ const hasTrustedPayload = transferTypes.has(TRANSFER_MIME_TYPE);
473
+
474
+ const rawData = hasTrustedPayload ? transfer.getData(TRANSFER_MIME_TYPE) : null;
475
+ const parsed = parseDragState(rawData);
476
+ const wasRejectedPayload = hasTrustedPayload && !parsed;
477
+
478
+ if (existing) {
479
+ if (parsed && parsed.creativeId === existing.creativeId && parsed.treeId === existing.treeId) {
480
+ return { draggedState: existing, isExternal: false, wasRejectedPayload };
481
+ }
482
+
483
+ if (parsed) {
484
+ return { draggedState: parsed, isExternal: true, wasRejectedPayload };
485
+ }
486
+
487
+ if (hasTrustedPayload) {
488
+ return { draggedState: null, isExternal: false, wasRejectedPayload };
489
+ }
490
+
491
+ return { draggedState: existing, isExternal: false, wasRejectedPayload };
492
+ }
493
+
494
+ if (!parsed) {
495
+ return { draggedState: null, isExternal: false, wasRejectedPayload };
496
+ }
497
+
498
+ return { draggedState: parsed, isExternal: true, wasRejectedPayload };
499
+ }
500
+
501
+ function notifyInvalidDrop() {
502
+ console.error('Rejected invalid creative drop payload');
503
+ if (typeof window !== 'undefined' && typeof window.alert === 'function') {
504
+ window.alert(INVALID_DROP_MESSAGE);
505
+ }
506
+ }
507
+
508
+ export function handleDragStart(event) {
509
+ const tree = event.target.closest(DRAGGABLE_SELECTOR);
510
+ if (!tree || tree.draggable === false) return;
511
+ const row = asTreeRow(tree);
512
+ if (!row) return;
513
+ const windowId = ensureWindowId();
514
+ const creativeId = row.getAttribute('creative-id');
515
+ const selectedCreativeIds = collectSelectedCreativeIds(creativeId);
516
+ setDraggedState({
517
+ tree,
518
+ row,
519
+ treeId: tree.id,
520
+ creativeId,
521
+ parentId: row.getAttribute('parent-id') || null,
522
+ level: Number(row.getAttribute('level') || row.level || 1),
523
+ isRoot: row.hasAttribute('is-root'),
524
+ sourceWindowId: windowId,
525
+ selectedCreativeIds,
526
+ });
527
+ event.dataTransfer.effectAllowed = 'move';
528
+
529
+ const sessionToken = ensureDragSessionToken();
530
+ const serialized = serializeDragState(getDraggedState(), sessionToken);
531
+ if (serialized) {
532
+ event.dataTransfer.setData(TRANSFER_MIME_TYPE, serialized);
533
+ event.dataTransfer.setData('text/plain', serialized);
534
+ }
535
+ }
536
+
537
+ export function handleDragOver(event) {
538
+ const tree = event.target.closest(DRAGGABLE_SELECTOR);
539
+ const lastRow = getLastDragOverRow();
540
+ if (lastRow && lastRow !== tree) {
541
+ clearDragHighlight(lastRow);
542
+ }
543
+ if (!tree || tree.draggable === false) return;
544
+ event.preventDefault();
545
+ event.dataTransfer.dropEffect = 'move';
546
+
547
+ const rect = tree.getBoundingClientRect();
548
+ const height = rect.height;
549
+ const relY = event.clientY - rect.top;
550
+
551
+ // Hysteresis buffer (12% of height or at least 12px to handle small items)
552
+ // Increased from 5% to provide more stability against tremors
553
+ const hysteresis = Math.max(12, height * 0.12);
554
+ const topLimit = height * childZoneRatio;
555
+ const bottomLimit = height * (1 - childZoneRatio);
556
+
557
+ const isTop = tree.classList.contains('drag-over-top');
558
+ const isBottom = tree.classList.contains('drag-over-bottom');
559
+ const isChild = tree.classList.contains('drag-over-child');
560
+
561
+ let effectiveTop = topLimit;
562
+ let effectiveBottom = bottomLimit;
563
+
564
+ // Apply hysteresis based on current state to create "stickiness"
565
+ if (isTop) {
566
+ effectiveTop += hysteresis;
567
+ } else if (isChild) {
568
+ effectiveTop -= hysteresis;
569
+ effectiveBottom += hysteresis;
570
+ } else if (isBottom) {
571
+ effectiveBottom -= hysteresis;
572
+ }
573
+
574
+ if (relY < effectiveTop) {
575
+ tree.classList.add('drag-over', 'drag-over-top');
576
+ tree.classList.remove('drag-over-bottom', 'drag-over-child', 'child-drop-indicator-active');
577
+ } else if (relY > effectiveBottom) {
578
+ tree.classList.add('drag-over', 'drag-over-bottom');
579
+ tree.classList.remove('drag-over-top', 'drag-over-child', 'child-drop-indicator-active');
580
+ } else {
581
+ tree.classList.add('drag-over', 'drag-over-child', 'child-drop-indicator-active');
582
+ tree.classList.remove('drag-over-top', 'drag-over-bottom');
583
+ }
584
+
585
+ if (event.shiftKey) {
586
+ showLinkHover(event.clientX, event.clientY);
587
+ } else {
588
+ hideLinkHover();
589
+ }
590
+
591
+ setLastDragOverRow(tree);
592
+ }
593
+
594
+ function resetDrag() {
595
+ resetDraggedState();
596
+ hideLinkHover();
597
+ }
598
+
599
+ export function handleDrop(event) {
600
+ const targetTree = event.target.closest(DRAGGABLE_SELECTOR);
601
+ const targetId = targetTree ? targetTree.id : '';
602
+
603
+ // Capture visual state before clearing highlights to ensure WYSIWYG
604
+ const isVisualTop = targetTree && targetTree.classList.contains('drag-over-top');
605
+ const isVisualBottom = targetTree && targetTree.classList.contains('drag-over-bottom');
606
+ const isVisualChild = targetTree && targetTree.classList.contains('drag-over-child');
607
+
608
+ clearDragHighlight(targetTree);
609
+ clearDragHighlight(getLastDragOverRow());
610
+
611
+ const { draggedState, isExternal, wasRejectedPayload } = getDraggedContext(event);
612
+
613
+ if (!targetTree || targetTree.draggable === false) {
614
+ resetDrag();
615
+ return;
616
+ }
617
+
618
+ if (!draggedState) {
619
+ if (wasRejectedPayload) {
620
+ notifyInvalidDrop();
621
+ }
622
+ resetDrag();
623
+ return;
624
+ }
625
+
626
+ event.preventDefault();
627
+
628
+ if (!targetId || draggedState.treeId === targetId) {
629
+ resetDrag();
630
+ return;
631
+ }
632
+
633
+ const targetRow = asTreeRow(targetTree);
634
+ const resolvedDraggedState =
635
+ isExternal && draggedState ? resolveDraggedStateFromDom(draggedState) : draggedState;
636
+ const hasDomState = !!resolvedDraggedState?.row && !!resolvedDraggedState?.tree;
637
+ const draggedRow = hasDomState ? resolvedDraggedState.row : null;
638
+ if (!targetRow) {
639
+ resetDrag();
640
+ return;
641
+ }
642
+
643
+ const baseDraggedState = resolvedDraggedState || draggedState;
644
+ const draggedIds = resolveDraggedIds(baseDraggedState);
645
+ const isMultiDrag = draggedIds.length > 1;
646
+
647
+ if (draggedRow && isDescendantRow(draggedRow, targetRow)) {
648
+ resetDrag();
649
+ return;
650
+ }
651
+
652
+ if (isMultiDrag) {
653
+ const targetCreativeId = targetRow.getAttribute('creative-id');
654
+ if (draggedIds.includes(String(targetCreativeId))) {
655
+ resetDrag();
656
+ return;
657
+ }
658
+
659
+ if (typeof document !== 'undefined') {
660
+ const selectedRows = draggedIds
661
+ .map((id) => {
662
+ if (draggedRow && String(resolvedDraggedState?.creativeId) === String(id)) {
663
+ return draggedRow;
664
+ }
665
+ const treeElement = document.getElementById(`creative-${id}`);
666
+ return treeElement ? asTreeRow(treeElement) : null;
667
+ })
668
+ .filter(Boolean);
669
+
670
+ const targetWithinSelection = selectedRows.some(
671
+ (rowEl) => rowEl === targetRow || isDescendantRow(rowEl, targetRow)
672
+ );
673
+
674
+ if (targetWithinSelection) {
675
+ resetDrag();
676
+ return;
677
+ }
678
+ }
679
+ }
680
+
681
+ let direction;
682
+ if (isVisualTop) {
683
+ direction = 'up';
684
+ } else if (isVisualBottom) {
685
+ direction = 'down';
686
+ } else if (isVisualChild) {
687
+ direction = 'child';
688
+ } else {
689
+ // Fallback calculation
690
+ const rect = targetTree.getBoundingClientRect();
691
+ const height = rect.height;
692
+ const relY = event.clientY - rect.top;
693
+ const topLimit = height * childZoneRatio;
694
+ const bottomLimit = height * (1 - childZoneRatio);
695
+
696
+ if (relY < topLimit) {
697
+ direction = 'up';
698
+ } else if (relY > bottomLimit) {
699
+ direction = 'down';
700
+ } else {
701
+ direction = 'child';
702
+ }
703
+ }
704
+
705
+ if (event.shiftKey) {
706
+ const snapshot = { ...draggedState };
707
+ resetDrag();
708
+
709
+ if (isMultiDrag) {
710
+ // Handle multiple linked creatives
711
+ const promises = draggedIds.map(draggedId =>
712
+ sendLinkedCreative({
713
+ draggedId,
714
+ targetId: targetId.replace('creative-', ''),
715
+ direction,
716
+ })
717
+ );
718
+
719
+ Promise.all(promises)
720
+ .then(() => window.location.reload())
721
+ .catch((error) => console.error('Failed to create linked creatives', error));
722
+ } else {
723
+ // Handle single linked creative
724
+ sendLinkedCreative({
725
+ draggedId: snapshot.creativeId,
726
+ targetId: targetId.replace('creative-', ''),
727
+ direction,
728
+ })
729
+ .then(() => window.location.reload())
730
+ .catch((error) => console.error('Failed to create linked creative', error));
731
+ }
732
+ return;
733
+ }
734
+
735
+ let moveContext = null;
736
+ let newParentId = null;
737
+ let draggedChildren = null;
738
+
739
+ if (draggedRow && !isMultiDrag) {
740
+ draggedChildren = getChildrenContainer(draggedRow);
741
+ moveContext = createMoveContext(
742
+ resolvedDraggedState,
743
+ targetRow,
744
+ draggedChildren
745
+ );
746
+
747
+ ({ newParentId } = applyMove({
748
+ direction,
749
+ targetRow,
750
+ draggedState: resolvedDraggedState,
751
+ draggedChildren,
752
+ moveContext,
753
+ }));
754
+ }
755
+
756
+ const draggedNumericId = draggedState.creativeId;
757
+ const dropSignalDetails = isMultiDrag
758
+ ? null
759
+ : {
760
+ creativeId: draggedNumericId,
761
+ treeId: draggedState.treeId,
762
+ sourceWindowId: draggedState.sourceWindowId,
763
+ targetTreeId: targetId,
764
+ direction,
765
+ };
766
+
767
+ resetDrag();
768
+
769
+ const shouldReloadOnFinalize = isExternal && !moveContext;
770
+
771
+ const finalizeDrop = () => {
772
+ if (shouldReloadOnFinalize) {
773
+ window.location.reload();
774
+ }
775
+ };
776
+
777
+ const reorderPayload = {
778
+ targetId: targetId.replace('creative-', ''),
779
+ direction,
780
+ };
781
+
782
+ if (isMultiDrag) {
783
+ reorderPayload.draggedIds = draggedIds;
784
+ } else {
785
+ reorderPayload.draggedId = draggedNumericId;
786
+ }
787
+
788
+ sendNewOrder(reorderPayload)
789
+ .then((response) => {
790
+ if (!response.ok) {
791
+ if (moveContext) {
792
+ revertMove(moveContext, newParentId);
793
+ }
794
+ return;
795
+ }
796
+
797
+ if (isMultiDrag) {
798
+ window.location.reload();
799
+ return;
800
+ }
801
+
802
+ if (dropSignalDetails?.sourceWindowId) {
803
+ emitDropSignal(dropSignalDetails);
804
+ dispatchDropCompletion({ ...dropSignalDetails, context: 'target' });
805
+ }
806
+ })
807
+ .catch((error) => {
808
+ console.error('Failed to update order', error);
809
+ if (moveContext) {
810
+ revertMove(moveContext, newParentId);
811
+ }
812
+ })
813
+ .finally(finalizeDrop);
814
+ }
815
+
816
+ export function handleDragLeave(event) {
817
+ const tree = event.target.closest(DRAGGABLE_SELECTOR);
818
+ if (!tree || tree.draggable === false) return;
819
+ clearDragHighlight(tree);
820
+ hideLinkHover();
821
+ }
822
+
823
+ function handleDragEnd() {
824
+ resetDrag();
825
+ }
826
+
827
+ export function addGlobalListeners() {
828
+ document.addEventListener('dragend', handleDragEnd);
829
+ window.addEventListener('storage', handleStorageChange);
830
+ window.addEventListener(DROP_COMPLETED_EVENT, handleDropCompletionEvent);
831
+ }
832
+
833
+ export function removeGlobalListeners() {
834
+ document.removeEventListener('dragend', handleDragEnd);
835
+ window.removeEventListener('storage', handleStorageChange);
836
+ window.removeEventListener(DROP_COMPLETED_EVENT, handleDropCompletionEvent);
837
+ }
838
+
839
+ export function registerGlobalHandlers() {
840
+ initIndicator();
841
+ addGlobalListeners();
842
+ }
843
+
844
+ export function hasActiveDrag() {
845
+ return hasDraggedState();
846
+ }