collavre 0.1.1 → 0.2.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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/comments_popup.css +293 -8
  3. data/app/assets/stylesheets/collavre/mention_menu.css +26 -0
  4. data/app/assets/stylesheets/collavre/popup.css +7 -0
  5. data/app/assets/stylesheets/collavre/print.css +18 -0
  6. data/app/channels/collavre/comments_presence_channel.rb +33 -0
  7. data/app/components/collavre/autocomplete_popup_component.html.erb +3 -0
  8. data/app/components/collavre/autocomplete_popup_component.rb +18 -0
  9. data/app/components/collavre/command_menu_component.rb +7 -0
  10. data/app/components/collavre/plans_timeline_component.html.erb +1 -1
  11. data/app/components/collavre/plans_timeline_component.rb +29 -32
  12. data/app/components/collavre/user_mention_menu_component.rb +4 -5
  13. data/app/controllers/collavre/comments_controller.rb +111 -10
  14. data/app/controllers/collavre/creatives_controller.rb +8 -0
  15. data/app/controllers/collavre/google_auth_controller.rb +5 -1
  16. data/app/controllers/collavre/plans_controller.rb +65 -9
  17. data/app/controllers/collavre/topics_controller.rb +42 -0
  18. data/app/controllers/collavre/users_controller.rb +4 -14
  19. data/app/errors/collavre/approval_pending_error.rb +54 -0
  20. data/app/errors/collavre/cancelled_error.rb +9 -0
  21. data/app/helpers/collavre/navigation_helper.rb +3 -1
  22. data/app/javascript/collavre.js +1 -0
  23. data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +2 -1
  24. data/app/javascript/controllers/comments/form_controller.js +2 -1
  25. data/app/javascript/controllers/comments/list_controller.js +185 -2
  26. data/app/javascript/controllers/comments/popup_controller.js +95 -20
  27. data/app/javascript/controllers/comments/presence_controller.js +30 -1
  28. data/app/javascript/controllers/comments/topics_controller.js +314 -4
  29. data/app/javascript/modules/__tests__/creative_progress.test.js +50 -0
  30. data/app/javascript/modules/command_menu.js +116 -0
  31. data/app/javascript/modules/creative_progress.js +14 -0
  32. data/app/javascript/modules/creative_row_editor.js +104 -20
  33. data/app/javascript/modules/plans_timeline.js +15 -4
  34. data/app/javascript/modules/share_modal.js +3 -0
  35. data/app/jobs/collavre/ai_agent_job.rb +35 -21
  36. data/app/models/collavre/calendar_event.rb +7 -1
  37. data/app/models/collavre/comment.rb +35 -2
  38. data/app/models/collavre/creative.rb +1 -3
  39. data/app/models/collavre/mcp_tool.rb +4 -0
  40. data/app/models/collavre/plan.rb +23 -0
  41. data/app/models/collavre/topic.rb +12 -0
  42. data/app/models/collavre/user.rb +15 -1
  43. data/app/services/collavre/ai_agent_service.rb +174 -66
  44. data/app/services/collavre/ai_client.rb +31 -2
  45. data/app/services/collavre/comments/action_executor.rb +47 -1
  46. data/app/services/collavre/comments/calendar_command.rb +117 -18
  47. data/app/services/collavre/google_calendar_service.rb +38 -15
  48. data/app/services/collavre/markdown_importer.rb +47 -8
  49. data/app/services/collavre/mcp_service.rb +23 -10
  50. data/app/services/collavre/system_events/router.rb +50 -26
  51. data/app/services/collavre/tools/creative_create_service.rb +97 -0
  52. data/app/services/collavre/tools/creative_update_service.rb +116 -0
  53. data/app/views/collavre/comments/_comment.html.erb +2 -2
  54. data/app/views/collavre/comments/_comments_popup.html.erb +40 -6
  55. data/app/views/collavre/comments/fullscreen.html.erb +5 -0
  56. data/app/views/collavre/creatives/_inline_edit_form.html.erb +11 -3
  57. data/app/views/collavre/creatives/_integration_modals.html.erb +6 -0
  58. data/app/views/collavre/creatives/_integration_triggers.html.erb +8 -0
  59. data/app/views/collavre/creatives/_integrations_menu.html.erb +12 -0
  60. data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +13 -1
  61. data/app/views/collavre/creatives/_share_button.html.erb +1 -1
  62. data/app/views/collavre/creatives/index.html.erb +22 -4
  63. data/app/views/collavre/users/edit_ai.html.erb +15 -0
  64. data/app/views/collavre/users/new_ai.html.erb +15 -0
  65. data/app/views/layouts/collavre/chat.html.erb +46 -0
  66. data/config/locales/ai_agent.en.yml +15 -0
  67. data/config/locales/ai_agent.ko.yml +15 -0
  68. data/config/locales/comments.en.yml +15 -3
  69. data/config/locales/comments.ko.yml +15 -3
  70. data/config/locales/creatives.en.yml +3 -31
  71. data/config/locales/creatives.ko.yml +3 -27
  72. data/config/locales/plans.en.yml +4 -0
  73. data/config/locales/plans.ko.yml +4 -0
  74. data/config/locales/users.en.yml +3 -0
  75. data/config/locales/users.ko.yml +3 -0
  76. data/config/routes.rb +8 -3
  77. data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +1 -1
  78. data/db/migrate/20260131100000_migrate_active_storage_attachment_record_types.rb +21 -0
  79. data/db/migrate/20260201100000_make_google_event_id_nullable.rb +5 -0
  80. data/lib/collavre/engine.rb +171 -6
  81. data/lib/collavre/integration_registry.rb +129 -0
  82. data/lib/collavre/version.rb +1 -1
  83. data/lib/collavre.rb +2 -0
  84. data/lib/navigation/registry.rb +130 -0
  85. metadata +22 -15
  86. data/app/components/collavre/user_mention_menu_component.html.erb +0 -3
  87. data/app/controllers/collavre/notion_auth_controller.rb +0 -25
  88. data/app/jobs/collavre/notion_export_job.rb +0 -30
  89. data/app/jobs/collavre/notion_sync_job.rb +0 -48
  90. data/app/models/collavre/notion_account.rb +0 -17
  91. data/app/models/collavre/notion_block_link.rb +0 -10
  92. data/app/models/collavre/notion_page_link.rb +0 -19
  93. data/app/services/collavre/notion_client.rb +0 -231
  94. data/app/services/collavre/notion_creative_exporter.rb +0 -296
  95. data/app/services/collavre/notion_service.rb +0 -249
  96. data/app/views/collavre/creatives/_notion_integration_modal.html.erb +0 -90
  97. data/db/migrate/20241201000000_create_notion_integrations.rb +0 -29
  98. data/db/migrate/20250312000000_create_notion_block_links.rb +0 -16
  99. data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d03302f37a546f2c0e067497346b90cf7ec6575526494441dfd9425524d48ca
4
- data.tar.gz: 2f3b84e4096620cf8cdf06511ee621872c6c8dd96ef687dff56eb8e4a33a625d
3
+ metadata.gz: a12dcc4af49e744b199e83be2b283e804d62e67e2b817a0c4da80494eb558357
4
+ data.tar.gz: a1f93a9d7a43bf370af9fe47044a6613e50ee258b55f4cd7609b17245c5ffef4
5
5
  SHA512:
6
- metadata.gz: 74d470294f2ffbddb9201a09b8fb43d294a13de026a696964c1f6a0bb3f12df46c96c03d99fbf03304e91d04419ceb8419c52ae124aec3b671ca887ba99cc5e5
7
- data.tar.gz: 7f677490ae7b3b49fb20651d981b613cccb5f37aa0edb0a634bc54d1be302d4f7e1f9aeee206dadc6fd73f82aa0ccbe2858efd2fbac50b443a25b8b00288ac12
6
+ metadata.gz: c7c872e1e41caaa91ca147174e40590099612338fe56d143c65fefdb522c0ea3d26e087d1d35e0ec9f214479b6f4103e984c430ed18de3976dd8e28beabeafb1
7
+ data.tar.gz: 1ad2496caec59b6bf8d85bae5b90797a5a4de0087cda9d9f962501db426928c76b4b3a7aaefc3724dcfa26e3546d1afbe45c56a7ed9573dbd5129c1c177e3a6d
@@ -13,6 +13,78 @@
13
13
  max-width: calc(100vw - 0.5em) !important;
14
14
  }
15
15
 
16
+ body.chat-fullscreen {
17
+ margin: 0;
18
+ padding: 0;
19
+ height: 100vh;
20
+ overflow: hidden;
21
+ }
22
+
23
+ body.chat-fullscreen main {
24
+ height: 100vh;
25
+ width: 100vw;
26
+ max-width: none;
27
+ padding: 0;
28
+ margin: 0;
29
+ box-sizing: border-box;
30
+ }
31
+
32
+ .comments-fullscreen-page {
33
+ height: 100vh;
34
+ width: 100vw;
35
+ padding: 0;
36
+ margin: 0;
37
+ box-sizing: border-box;
38
+ }
39
+
40
+ .comments-fullscreen-page #comments-popup,
41
+ #comments-popup[data-fullscreen="true"] {
42
+ display: flex !important;
43
+ position: static;
44
+ width: 100%;
45
+ height: 100%;
46
+ max-width: none !important;
47
+ max-height: none !important;
48
+ border-radius: 0;
49
+ box-shadow: none;
50
+ border: none;
51
+ z-index: auto;
52
+ box-sizing: border-box;
53
+ padding: 1em;
54
+ }
55
+
56
+ .comments-popup-header {
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: space-between;
60
+ gap: 0.75em;
61
+ }
62
+
63
+ .comments-popup-actions {
64
+ display: inline-flex;
65
+ align-items: center;
66
+ gap: 0.4em;
67
+ }
68
+
69
+ .comments-popup-action {
70
+ background: none;
71
+ border: none;
72
+ color: var(--color-text);
73
+ cursor: pointer;
74
+ padding: 0;
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ font-size: 1.2em;
79
+ line-height: 1;
80
+ }
81
+
82
+ .comments-popup-action-icon {
83
+ width: 16px;
84
+ height: 16px;
85
+ fill: currentColor;
86
+ }
87
+
16
88
  #comments-popup .resize-handle {
17
89
  position: absolute;
18
90
  width: 10px;
@@ -51,10 +123,20 @@
51
123
 
52
124
  .comment-topics-list {
53
125
  display: flex;
54
- flex-wrap: wrap;
126
+ flex-wrap: nowrap;
127
+ align-items: center;
55
128
  gap: 0.5em;
56
- padding-bottom: 0.5em;
129
+ padding: 0.5em 0;
57
130
  margin-bottom: 0.5em;
131
+ overflow-x: auto;
132
+ overflow-y: hidden;
133
+ scrollbar-width: none; /* Firefox */
134
+ -ms-overflow-style: none; /* IE/Edge */
135
+ min-height: 36px; /* Prevent height collapse during scroll */
136
+ }
137
+
138
+ .comment-topics-list::-webkit-scrollbar {
139
+ display: none; /* Chrome/Safari */
58
140
  }
59
141
 
60
142
  #comments-list::before {
@@ -353,8 +435,29 @@
353
435
 
354
436
  .comment-status-label {
355
437
  margin-left: 0.4em;
356
- font-size: 0.85em;
357
- color: var(--color-muted);
438
+ font-size: 0.75em;
439
+ padding: 0.15em 0.5em;
440
+ border-radius: 0.8em;
441
+ font-weight: 500;
442
+ display: inline-flex;
443
+ align-items: center;
444
+ gap: 0.25em;
445
+ vertical-align: middle;
446
+ }
447
+
448
+ .comment-status-label.private-label {
449
+ background-color: color-mix(in srgb, var(--color-badge-bg) 20%, transparent);
450
+ color: var(--color-badge-bg);
451
+ }
452
+
453
+ .comment-status-label.approved-label {
454
+ background-color: color-mix(in srgb, var(--color-complete) 20%, transparent);
455
+ color: var(--color-complete);
456
+ }
457
+
458
+ .comment-status-label.pending-label {
459
+ background-color: color-mix(in srgb, orange 20%, transparent);
460
+ color: orange;
358
461
  }
359
462
 
360
463
  .comment-action-container {
@@ -401,6 +504,7 @@
401
504
  #voice-comments-btn,
402
505
  #attach-image-btn,
403
506
  #cancel-edit-btn,
507
+ #new-comment-form button[type="submit"],
404
508
  .convert-comment-btn,
405
509
  .delete-comment-btn,
406
510
  .edit-comment-btn,
@@ -511,6 +615,18 @@
511
615
  #comments-popup.open {
512
616
  transform: translateY(0);
513
617
  }
618
+
619
+ .comments-fullscreen-page #comments-popup,
620
+ #comments-popup[data-fullscreen="true"] {
621
+ display: flex !important;
622
+ transform: none;
623
+ border-radius: 0;
624
+ padding: 1em;
625
+ position: static;
626
+ width: 100%;
627
+ height: 100%;
628
+ box-sizing: border-box;
629
+ }
514
630
  }
515
631
 
516
632
  .comment-item.highlight-flash {
@@ -530,6 +646,38 @@
530
646
  #comment-participants {
531
647
  margin-bottom: 0.3em;
532
648
  min-height: 20px;
649
+ display: flex;
650
+ align-items: center;
651
+ flex-wrap: wrap;
652
+ gap: 0.2em;
653
+ }
654
+
655
+ #comment-participants .avatar-wrapper {
656
+ display: inline-flex;
657
+ align-items: center;
658
+ justify-content: center;
659
+ }
660
+
661
+ .add-participant-btn {
662
+ display: inline-flex;
663
+ align-items: center;
664
+ justify-content: center;
665
+ width: 20px;
666
+ height: 20px;
667
+ border-radius: 50%;
668
+ border: 1px solid var(--color-border);
669
+ background: transparent;
670
+ cursor: pointer;
671
+ color: var(--color-muted);
672
+ font-size: 1.2em;
673
+ line-height: 1;
674
+ padding: 0;
675
+ flex-shrink: 0;
676
+ }
677
+
678
+ .add-participant-btn:hover {
679
+ background: var(--color-section-bg);
680
+ color: var(--color-text);
533
681
  }
534
682
 
535
683
  #typing-indicator {
@@ -547,7 +695,7 @@
547
695
  }
548
696
 
549
697
  .comment-presence-avatar {
550
- margin-right: 0.2em;
698
+ margin-right: 0;
551
699
  }
552
700
 
553
701
  .comment-presence-avatar.inactive {
@@ -562,15 +710,26 @@
562
710
 
563
711
  .comment-topics-list {
564
712
  display: flex;
565
- flex-wrap: wrap;
713
+ flex-wrap: nowrap;
714
+ align-items: center;
566
715
  gap: 0.5em;
567
- padding-bottom: 0.5em;
716
+ padding: 0.5em 0;
568
717
  margin-bottom: 0.5em;
718
+ overflow-x: auto;
719
+ overflow-y: hidden;
720
+ scrollbar-width: none; /* Firefox */
721
+ -ms-overflow-style: none; /* IE/Edge */
722
+ min-height: 36px; /* Prevent height collapse during scroll */
723
+ }
724
+
725
+ .comment-topics-list::-webkit-scrollbar {
726
+ display: none; /* Chrome/Safari */
569
727
  }
570
728
 
571
729
  .topic-tag {
572
730
  display: inline-flex;
573
731
  align-items: center;
732
+ flex-shrink: 0;
574
733
  gap: 4px;
575
734
  padding: 0.2em 0.6em;
576
735
  border-radius: 12px;
@@ -580,6 +739,9 @@
580
739
  font-size: 0.85em;
581
740
  cursor: pointer;
582
741
  transition: all 0.2s;
742
+ white-space: nowrap;
743
+ height: 26px; /* Fixed height to prevent shrinking */
744
+ box-sizing: border-box;
583
745
  }
584
746
 
585
747
  .topic-tag:hover {
@@ -610,6 +772,67 @@
610
772
  font-weight: bold;
611
773
  }
612
774
 
775
+ /* Drag and drop styles for moving comments to topics */
776
+ .topic-tag.drag-over {
777
+ background-color: var(--color-accent, #007bff);
778
+ color: white;
779
+ border-color: var(--color-accent, #007bff);
780
+ transform: scale(1.05);
781
+ transition: all 0.15s ease;
782
+ }
783
+
784
+ .topic-drop-target {
785
+ transition: all 0.15s ease;
786
+ }
787
+
788
+ /* Topic reorder drag styles */
789
+ .topic-tag[draggable="true"] {
790
+ cursor: grab;
791
+ }
792
+
793
+ .topic-tag[draggable="true"]:active {
794
+ cursor: grabbing;
795
+ }
796
+
797
+ .topic-tag.topic-dragging {
798
+ opacity: 0.5;
799
+ }
800
+
801
+ .topic-tag.topic-drag-over-left {
802
+ border-left: 3px solid var(--color-accent, #007bff);
803
+ margin-left: -1px;
804
+ }
805
+
806
+ .topic-tag.topic-drag-over-right {
807
+ border-right: 3px solid var(--color-accent, #007bff);
808
+ margin-right: -1px;
809
+ }
810
+
811
+ .comment-item[draggable="true"] {
812
+ cursor: grab;
813
+ }
814
+
815
+ .comment-item[draggable="true"]:active {
816
+ cursor: grabbing;
817
+ }
818
+
819
+ #comments-list.dragging-comments .comment-item:not(.selected-for-move) {
820
+ opacity: 0.5;
821
+ }
822
+
823
+ .comment-drag-image {
824
+ position: fixed;
825
+ top: -1000px;
826
+ left: -1000px;
827
+ padding: 8px 16px;
828
+ background-color: var(--color-accent, #007bff);
829
+ color: white;
830
+ border-radius: 4px;
831
+ font-size: 0.9em;
832
+ font-weight: 500;
833
+ white-space: nowrap;
834
+ }
835
+
613
836
  /* Hide the topic link if the message is displayed within that topic's context */
614
837
  /* Use Javascript to set data-current-topic-id on the list container */
615
838
  .comment-topic-link {
@@ -657,6 +880,12 @@
657
880
  display: none;
658
881
  }
659
882
 
883
+ .topic-creation-container {
884
+ display: inline-flex;
885
+ align-items: center;
886
+ flex-shrink: 0;
887
+ }
888
+
660
889
  .add-topic-btn {
661
890
  display: inline-flex;
662
891
  align-items: center;
@@ -689,4 +918,60 @@
689
918
  font-size: 0.85em;
690
919
  width: 100px;
691
920
  outline: none;
692
- }
921
+ }
922
+
923
+ .topic-edit-input {
924
+ display: inline-block;
925
+ padding: 0.1em 0.4em;
926
+ border-radius: 8px;
927
+ background: var(--color-bg);
928
+ border: 1px solid var(--color-accent, #007bff);
929
+ color: var(--color-text);
930
+ font-size: 0.85em;
931
+ width: 80px;
932
+ outline: none;
933
+ }
934
+
935
+ /* Selection hint popup */
936
+ .selection-hint-popup {
937
+ position: fixed;
938
+ background: var(--color-bg-secondary, #f8f9fa);
939
+ border: 1px solid var(--color-border, #dee2e6);
940
+ border-radius: 8px;
941
+ padding: 8px 12px;
942
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
943
+ white-space: nowrap;
944
+ z-index: 10000;
945
+ animation: hint-fade-in 0.2s ease-out;
946
+ }
947
+
948
+ .selection-hint-popup .hint-content {
949
+ display: flex;
950
+ flex-direction: column;
951
+ gap: 4px;
952
+ font-size: 0.8em;
953
+ color: var(--color-text-secondary, #6c757d);
954
+ }
955
+
956
+ .selection-hint-popup .hint-content span {
957
+ display: block;
958
+ }
959
+
960
+ @keyframes hint-fade-in {
961
+ from {
962
+ opacity: 0;
963
+ transform: translateY(-4px);
964
+ }
965
+ to {
966
+ opacity: 1;
967
+ transform: translateY(0);
968
+ }
969
+ }
970
+
971
+ /* Dark mode support */
972
+ @media (prefers-color-scheme: dark) {
973
+ .selection-hint-popup {
974
+ background: var(--color-bg-secondary, #2d2d2d);
975
+ border-color: var(--color-border, #444);
976
+ }
977
+ }
@@ -41,3 +41,29 @@
41
41
  padding: 0.35em 0.5em;
42
42
  cursor: pointer;
43
43
  }
44
+
45
+ .command-item {
46
+ display: flex;
47
+ gap: 0.5em;
48
+ align-items: baseline;
49
+ }
50
+
51
+ .command-label {
52
+ font-weight: 600;
53
+ }
54
+
55
+ .command-args {
56
+ font-size: 0.85em;
57
+ color: var(--color-text-muted);
58
+ }
59
+
60
+ .command-aliases {
61
+ font-size: 0.85em;
62
+ color: var(--color-text-muted);
63
+ }
64
+
65
+ .command-description {
66
+ margin-top: 0.2em;
67
+ font-size: 0.85em;
68
+ color: var(--color-text-muted);
69
+ }
@@ -138,6 +138,13 @@
138
138
  right: 0;
139
139
  }
140
140
 
141
+ .popup-menu-item button,
142
+ .popup-menu-item a {
143
+ display: block;
144
+ width: 100%;
145
+ text-align: left;
146
+ }
147
+
141
148
  #user-menu button {
142
149
  display: block;
143
150
  text-align: left;
@@ -34,4 +34,22 @@
34
34
  .creative-actions-row {
35
35
  visibility: visible !important;
36
36
  }
37
+
38
+ .creative-tree-bullet {
39
+ background: none;
40
+ width: auto;
41
+ height: auto;
42
+ min-width: 0;
43
+ min-height: 0;
44
+ margin-top: 0.1em;
45
+ }
46
+
47
+ .creative-tree-bullet::before {
48
+ content: "•";
49
+ display: inline-block;
50
+ color: var(--color-text);
51
+ font-size: 0.9em;
52
+ line-height: 1;
53
+ width: 0.6em;
54
+ }
37
55
  }
@@ -1,5 +1,37 @@
1
1
  module Collavre
2
2
  class CommentsPresenceChannel < ApplicationCable::Channel
3
+ # Broadcast status for any currently running AI agent tasks for a creative.
4
+ # Called when a user subscribes to ensure they see ongoing agent activity.
5
+ def self.broadcast_running_agents(creative_id)
6
+ Task.where(status: %w[running pending]).find_each do |task|
7
+ task_creative_id = task.trigger_event_payload&.dig("creative", "id")
8
+ next unless task_creative_id == creative_id
9
+
10
+ broadcast_agent_status(
11
+ creative_id,
12
+ status: "thinking",
13
+ agent_id: task.agent_id,
14
+ agent_name: task.agent.display_name,
15
+ task_id: task.id
16
+ )
17
+ end
18
+ end
19
+
20
+ # Broadcast agent status (thinking/streaming/idle) to presence channel.
21
+ # This allows the frontend typing indicator to show AI agent activity.
22
+ def self.broadcast_agent_status(creative_id, status:, agent_id:, agent_name:, task_id: nil, content: nil)
23
+ payload = {
24
+ agent_status: {
25
+ id: agent_id,
26
+ name: agent_name,
27
+ status: status,
28
+ task_id: task_id
29
+ }
30
+ }
31
+ payload[:agent_status][:content] = content if content.present?
32
+ ActionCable.server.broadcast("comments_presence:#{creative_id}", payload)
33
+ end
34
+
3
35
  def subscribed
4
36
  Rails.logger.info "User #{current_user&.email} subscribed to comments presence for creative #{params[:creative_id]}"
5
37
  return unless params[:creative_id].present? && current_user
@@ -10,6 +42,7 @@ class CommentsPresenceChannel < ApplicationCable::Channel
10
42
  CommentPresenceStore.add(@creative_id, current_user.id)
11
43
  Comment.broadcast_badge(creative, current_user)
12
44
  broadcast_presence
45
+ CommentsPresenceChannel.broadcast_running_agents(@creative_id)
13
46
  end
14
47
 
15
48
  def unsubscribed
@@ -0,0 +1,3 @@
1
+ <div id="<%= menu_id %>" class="<%= css_classes %>" style="display:none;">
2
+ <ul class="<%= list_classes %>" data-popup-list></ul>
3
+ </div>
@@ -0,0 +1,18 @@
1
+ module Collavre
2
+ class AutocompletePopupComponent < ViewComponent::Base
3
+ def initialize(menu_id:, extra_classes: nil)
4
+ @menu_id = menu_id
5
+ @extra_classes = extra_classes
6
+ end
7
+
8
+ attr_reader :menu_id, :extra_classes
9
+
10
+ def css_classes
11
+ [ "common-popup", extra_classes ].compact.join(" ")
12
+ end
13
+
14
+ def list_classes
15
+ [ "common-popup-list", extra_classes ? "#{extra_classes.split.first}-results" : nil ].compact.join(" ")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ module Collavre
2
+ class CommandMenuComponent < AutocompletePopupComponent
3
+ def initialize(menu_id: "command-menu")
4
+ super(menu_id: menu_id, extra_classes: nil)
5
+ end
6
+ end
7
+ end
@@ -7,8 +7,8 @@
7
7
  <%= form_with(model: Plan.new, url: helpers.collavre.plans_path, local: true, id: 'new-plan-form') do |form| %>
8
8
  <%= form.hidden_field :creative_id, id: 'plan-creative-id' %>
9
9
  <input type="text" id="plan-select-creative-input" placeholder="<%= t('collavre.plans.select_creative', default: 'Select Creative') %>" autocomplete="off">
10
+ <%= form.date_field :start_date, placeholder: t('collavre.plans.start_date'), id: 'plan-start-date' %>
10
11
  <%= form.date_field :target_date, placeholder: t('collavre.plans.target_date'), id: 'plan-target-date' %>
11
12
  <%= form.submit t('collavre.plans.add_plan'), id: 'add-plan-btn', disabled: true %>
12
13
  <% end %>
13
14
  </div>
14
-
@@ -1,50 +1,47 @@
1
1
  module Collavre
2
2
  class PlansTimelineComponent < ViewComponent::Base
3
+ # Accepts pre-filtered plans and calendar_events from the controller
3
4
  def initialize(plans:, calendar_events: CalendarEvent.none)
4
5
  @start_date = Date.current - 30
5
6
  @end_date = Date.current + 30
6
7
  @plans = plans
7
- .where("target_date >= ? AND created_at <= ?", @start_date, @end_date)
8
- .select { |plan| plan.readable_by?(Current.user) }
9
- .sort_by(&:created_at)
10
8
  @calendar_events = calendar_events
11
- .includes(:creative)
12
- .where("DATE(start_time) <= ? AND DATE(end_time) >= ?", @end_date, @start_date)
13
- .order(:start_time)
14
9
  end
15
10
 
16
- attr_reader :plans, :start_date, :end_date, :calendar_events
11
+ attr_reader :plans, :calendar_events, :start_date, :end_date
17
12
 
13
+ # Called after component enters render context - safe to use helpers here
18
14
  def plan_data
19
- @plan_data ||= begin
20
- plan_items = @plans.map do |plan|
21
- {
22
- id: plan.id,
23
- name: (plan.creative&.effective_description(nil, false) || plan.name.presence || I18n.l(plan.target_date)),
24
- created_at: plan.created_at.to_date,
25
- target_date: plan.target_date,
26
- progress: plan.progress,
27
- path: plan_creatives_path(plan),
28
- deletable: plan.owner_id == Current.user&.id
29
- }
30
- end
31
- event_items = @calendar_events.map do |event|
32
- {
33
- id: "calendar_event_#{event.id}",
34
- name: event.summary.presence || I18n.l(event.start_time.to_date),
35
- created_at: event.start_time.to_date,
36
- target_date: event.end_time.to_date,
37
- progress: event.creative&.progress || 0,
38
- path: event.creative ? helpers.collavre.creative_path(event.creative) : event.html_link,
39
- deletable: event.user_id == Current.user&.id
40
- }
41
- end
42
- plan_items + event_items
43
- end
15
+ @plan_data ||= @plans.map { |plan| plan_item(plan) } + @calendar_events.map { |event| calendar_item(event) }
44
16
  end
45
17
 
46
18
  private
47
19
 
20
+ def plan_item(plan)
21
+ {
22
+ id: plan.id,
23
+ name: (plan.creative&.effective_description(nil, false) || plan.name.presence || I18n.l(plan.target_date)),
24
+ created_at: plan.created_at.to_date,
25
+ start_date: plan.start_date,
26
+ target_date: plan.target_date,
27
+ progress: plan.progress,
28
+ path: plan_creatives_path(plan),
29
+ deletable: plan.owner_id == Current.user&.id
30
+ }
31
+ end
32
+
33
+ def calendar_item(event)
34
+ {
35
+ id: "calendar_event_#{event.id}",
36
+ name: event.summary.presence || I18n.l(event.start_time.to_date),
37
+ created_at: event.start_time.to_date,
38
+ target_date: event.end_time.to_date,
39
+ progress: event.creative&.progress || 0,
40
+ path: event.creative ? helpers.collavre.creative_path(event.creative) : event.html_link,
41
+ deletable: event.user_id == Current.user&.id
42
+ }
43
+ end
44
+
48
45
  def plan_creatives_path(plan)
49
46
  if helpers.params[:id].present?
50
47
  helpers.collavre.creative_path(helpers.params[:id], tags: [ plan.id ])
@@ -1,8 +1,7 @@
1
1
  module Collavre
2
- class UserMentionMenuComponent < ViewComponent::Base
3
- def initialize(menu_id: "mention-menu")
4
- @menu_id = menu_id
2
+ class UserMentionMenuComponent < AutocompletePopupComponent
3
+ def initialize(menu_id: "mention-menu")
4
+ super(menu_id: menu_id, extra_classes: "mention-popup")
5
+ end
5
6
  end
6
- attr_reader :menu_id
7
- end
8
7
  end