coplan-engine 0.1.3 → 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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/coplan/application.css +212 -85
- data/app/controllers/coplan/api/v1/comments_controller.rb +7 -7
- data/app/controllers/coplan/comment_threads_controller.rb +26 -72
- data/app/controllers/coplan/plans_controller.rb +1 -3
- data/app/javascript/controllers/coplan/comment_form_controller.js +14 -0
- data/app/javascript/controllers/coplan/comment_nav_controller.js +134 -0
- data/app/javascript/controllers/coplan/text_selection_controller.js +99 -161
- data/app/jobs/coplan/automated_review_job.rb +4 -4
- data/app/models/coplan/comment_thread.rb +13 -7
- data/app/policies/coplan/comment_thread_policy.rb +1 -1
- data/app/views/coplan/comment_threads/_new_comment_form.html.erb +2 -1
- data/app/views/coplan/comment_threads/_reply_form.html.erb +2 -1
- data/app/views/coplan/comment_threads/_thread.html.erb +3 -6
- data/app/views/coplan/comment_threads/_thread_popover.html.erb +58 -0
- data/app/views/coplan/plans/show.html.erb +22 -30
- data/app/views/layouts/coplan/application.html.erb +3 -0
- data/config/routes.rb +2 -2
- data/db/migrate/20260320145453_migrate_comment_thread_statuses.rb +31 -0
- data/lib/coplan/version.rb +1 -1
- metadata +5 -2
- data/app/javascript/controllers/coplan/tabs_controller.js +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e4bc136761076f3c7f12bf4e025f5bfd0c89831c718f50cd88a4e650bb402e8
|
|
4
|
+
data.tar.gz: a7739fea373261118928c4c488043ca9d2547eac95d4ceeee91f004585e3b317
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d984b75c02ce889706c1cf1080f61106f5e355d0626d69c502b7f29ec28ef99300ad249d0c57cd1823f46d064f62e641a2c733b60a3df6cab266fc17fef453d
|
|
7
|
+
data.tar.gz: f70083d0f8ce612a2608e7863fe7606aecefa40e402c582596eccc68576f9cf60e546efd5f98d30c2a59a180d81a2c97e1b831ad9277ec21f54569df3ebf2325
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
--space-2xl: 3rem;
|
|
32
32
|
|
|
33
33
|
/* Typography */
|
|
34
|
-
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
34
|
+
--font-sans: 'Lexend', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
35
35
|
--font-mono: "SF Mono", SFMono-Regular, ui-monospace, Menlo, monospace;
|
|
36
36
|
--text-sm: 0.875rem;
|
|
37
37
|
--text-base: 1rem;
|
|
@@ -514,7 +514,7 @@ img, svg {
|
|
|
514
514
|
|
|
515
515
|
.markdown-rendered p {
|
|
516
516
|
margin-bottom: var(--space-md);
|
|
517
|
-
line-height: 1.
|
|
517
|
+
line-height: 1.9;
|
|
518
518
|
}
|
|
519
519
|
|
|
520
520
|
.markdown-rendered ul,
|
|
@@ -525,7 +525,7 @@ img, svg {
|
|
|
525
525
|
|
|
526
526
|
.markdown-rendered li {
|
|
527
527
|
margin-bottom: var(--space-xs);
|
|
528
|
-
line-height: 1.
|
|
528
|
+
line-height: 1.9;
|
|
529
529
|
}
|
|
530
530
|
|
|
531
531
|
.markdown-rendered pre {
|
|
@@ -605,20 +605,37 @@ img, svg {
|
|
|
605
605
|
|
|
606
606
|
/* Anchor text highlights */
|
|
607
607
|
.anchor-highlight {
|
|
608
|
-
background: rgba(255, 213, 79, 0.3);
|
|
609
|
-
border-bottom: 2px solid rgba(255, 179, 0, 0.5);
|
|
610
608
|
cursor: pointer;
|
|
611
609
|
transition: background 0.2s;
|
|
612
610
|
}
|
|
613
611
|
|
|
614
|
-
.anchor-highlight
|
|
615
|
-
background: rgba(
|
|
612
|
+
.anchor-highlight--open {
|
|
613
|
+
background: rgba(108, 140, 255, 0.12);
|
|
614
|
+
border-bottom: 2px solid rgba(108, 140, 255, 0.6);
|
|
615
|
+
cursor: pointer;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.anchor-highlight--open:hover {
|
|
619
|
+
background: rgba(108, 140, 255, 0.22);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.anchor-highlight--resolved {
|
|
623
|
+
background: none;
|
|
624
|
+
border-bottom: none;
|
|
625
|
+
cursor: default;
|
|
626
|
+
pointer-events: none;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.plan-layout--show-resolved .anchor-highlight--resolved {
|
|
630
|
+
border-bottom: 1px dashed var(--color-text-muted);
|
|
631
|
+
cursor: pointer;
|
|
632
|
+
pointer-events: auto;
|
|
616
633
|
}
|
|
617
634
|
|
|
618
635
|
.anchor-highlight--active {
|
|
619
|
-
background: rgba(
|
|
620
|
-
border-bottom: 2px solid var(--color-
|
|
621
|
-
outline: 2px solid rgba(
|
|
636
|
+
background: rgba(108, 140, 255, 0.25);
|
|
637
|
+
border-bottom: 2px solid var(--color-primary);
|
|
638
|
+
outline: 2px solid rgba(108, 140, 255, 0.3);
|
|
622
639
|
outline-offset: 1px;
|
|
623
640
|
}
|
|
624
641
|
|
|
@@ -653,87 +670,35 @@ img, svg {
|
|
|
653
670
|
border-radius: 0 var(--radius) var(--radius) 0;
|
|
654
671
|
}
|
|
655
672
|
|
|
656
|
-
/* Plan layout
|
|
673
|
+
/* Plan layout */
|
|
657
674
|
.plan-layout {
|
|
658
675
|
position: relative;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
.plan-layout__content {
|
|
662
|
-
max-width: 60%;
|
|
663
|
-
position: relative;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
.plan-layout__sidebar {
|
|
667
|
-
position: absolute;
|
|
668
|
-
top: 0;
|
|
669
|
-
right: 0;
|
|
670
|
-
width: 22rem;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
@media (max-width: 900px) {
|
|
674
|
-
.plan-layout__content {
|
|
675
|
-
max-width: 100%;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
.plan-layout__sidebar {
|
|
679
|
-
position: static;
|
|
680
|
-
width: 100%;
|
|
681
|
-
margin-top: var(--space-lg);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
.sidebar-heading {
|
|
685
|
-
font-size: var(--text-lg);
|
|
686
|
-
font-weight: 700;
|
|
687
|
-
margin-bottom: var(--space-md);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/* Comment tabs */
|
|
691
|
-
.comment-tabs__nav {
|
|
692
676
|
display: flex;
|
|
693
677
|
gap: 0;
|
|
694
|
-
border-bottom: 2px solid var(--color-border);
|
|
695
|
-
margin-bottom: var(--space-md);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
.comment-tab {
|
|
699
|
-
padding: var(--space-xs) var(--space-md);
|
|
700
|
-
font-size: var(--text-sm);
|
|
701
|
-
font-weight: 600;
|
|
702
|
-
color: var(--color-text-muted);
|
|
703
|
-
background: none;
|
|
704
|
-
border: none;
|
|
705
|
-
border-bottom: 2px solid transparent;
|
|
706
|
-
margin-bottom: -2px;
|
|
707
|
-
cursor: pointer;
|
|
708
678
|
}
|
|
709
679
|
|
|
710
|
-
.
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
.comment-tab--active {
|
|
715
|
-
color: var(--color-primary);
|
|
716
|
-
border-bottom-color: var(--color-primary);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
.comment-tab__count {
|
|
720
|
-
background: var(--color-border);
|
|
721
|
-
color: var(--color-text-muted);
|
|
722
|
-
font-size: 0.7rem;
|
|
723
|
-
padding: 1px 6px;
|
|
724
|
-
border-radius: 10px;
|
|
725
|
-
margin-left: 4px;
|
|
680
|
+
.plan-layout__margin {
|
|
681
|
+
width: 40px;
|
|
682
|
+
flex-shrink: 0;
|
|
683
|
+
position: relative;
|
|
726
684
|
}
|
|
727
685
|
|
|
728
|
-
.
|
|
729
|
-
|
|
730
|
-
|
|
686
|
+
.plan-layout__content {
|
|
687
|
+
position: relative;
|
|
688
|
+
flex: 1;
|
|
689
|
+
min-width: 0;
|
|
731
690
|
}
|
|
732
691
|
|
|
733
692
|
/* Comment form */
|
|
734
693
|
.comment-form {
|
|
735
|
-
|
|
694
|
+
position: absolute;
|
|
695
|
+
z-index: 100;
|
|
696
|
+
width: 360px;
|
|
736
697
|
padding: var(--space-md);
|
|
698
|
+
background: var(--color-surface);
|
|
699
|
+
border: 1px solid var(--color-border);
|
|
700
|
+
border-radius: var(--radius);
|
|
701
|
+
box-shadow: var(--shadow-lg);
|
|
737
702
|
}
|
|
738
703
|
|
|
739
704
|
.comment-form .form-group {
|
|
@@ -757,13 +722,6 @@ img, svg {
|
|
|
757
722
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
758
723
|
}
|
|
759
724
|
|
|
760
|
-
/* Comment threads list */
|
|
761
|
-
.comment-threads-list {
|
|
762
|
-
display: flex;
|
|
763
|
-
flex-direction: column;
|
|
764
|
-
gap: var(--space-md);
|
|
765
|
-
}
|
|
766
|
-
|
|
767
725
|
/* Comment thread */
|
|
768
726
|
.comment-thread {
|
|
769
727
|
padding: var(--space-md);
|
|
@@ -960,3 +918,172 @@ img, svg {
|
|
|
960
918
|
color: var(--color-text-muted);
|
|
961
919
|
margin-top: var(--space-xs);
|
|
962
920
|
}
|
|
921
|
+
|
|
922
|
+
/* Thread popover data containers (hidden, provide data for JS) */
|
|
923
|
+
.thread-popover-data {
|
|
924
|
+
display: contents;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/* Thread popover (native HTML Popover API) */
|
|
928
|
+
.thread-popover {
|
|
929
|
+
position: fixed;
|
|
930
|
+
margin: 0;
|
|
931
|
+
padding: var(--space-md);
|
|
932
|
+
background: var(--color-surface);
|
|
933
|
+
border: 1px solid var(--color-border);
|
|
934
|
+
border-radius: var(--radius);
|
|
935
|
+
box-shadow: var(--shadow-lg);
|
|
936
|
+
width: 380px;
|
|
937
|
+
max-height: 60vh;
|
|
938
|
+
overflow-y: auto;
|
|
939
|
+
z-index: 200;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.thread-popover::backdrop {
|
|
943
|
+
background: transparent;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.thread-popover__header {
|
|
947
|
+
display: flex;
|
|
948
|
+
gap: var(--space-xs);
|
|
949
|
+
align-items: center;
|
|
950
|
+
margin-bottom: var(--space-sm);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.thread-popover__quote {
|
|
954
|
+
border-left: 3px solid rgba(108, 140, 255, 0.6);
|
|
955
|
+
padding: var(--space-xs) var(--space-sm);
|
|
956
|
+
margin: 0 0 var(--space-sm) 0;
|
|
957
|
+
font-size: var(--text-sm);
|
|
958
|
+
color: var(--color-text-muted);
|
|
959
|
+
background: rgba(108, 140, 255, 0.05);
|
|
960
|
+
border-radius: 0 var(--radius) var(--radius) 0;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.thread-popover__comments {
|
|
964
|
+
display: flex;
|
|
965
|
+
flex-direction: column;
|
|
966
|
+
gap: var(--space-xs);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.thread-popover__reply {
|
|
970
|
+
margin-top: var(--space-sm);
|
|
971
|
+
padding-top: var(--space-sm);
|
|
972
|
+
border-top: 1px solid var(--color-border);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
.thread-popover__reply textarea {
|
|
976
|
+
width: 100%;
|
|
977
|
+
padding: var(--space-sm);
|
|
978
|
+
font-family: var(--font-sans);
|
|
979
|
+
font-size: var(--text-sm);
|
|
980
|
+
border: 1px solid var(--color-border);
|
|
981
|
+
border-radius: var(--radius);
|
|
982
|
+
resize: vertical;
|
|
983
|
+
min-height: 3rem;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.thread-popover__reply textarea:focus {
|
|
987
|
+
outline: none;
|
|
988
|
+
border-color: var(--color-primary);
|
|
989
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.thread-popover__reply .form-group {
|
|
993
|
+
margin-bottom: var(--space-xs);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.thread-popover__actions {
|
|
997
|
+
display: flex;
|
|
998
|
+
gap: var(--space-xs);
|
|
999
|
+
margin-top: var(--space-sm);
|
|
1000
|
+
padding-top: var(--space-sm);
|
|
1001
|
+
border-top: 1px solid var(--color-border);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/* Margin dots */
|
|
1005
|
+
.margin-dot {
|
|
1006
|
+
position: absolute;
|
|
1007
|
+
width: 12px;
|
|
1008
|
+
height: 12px;
|
|
1009
|
+
border-radius: 50%;
|
|
1010
|
+
cursor: pointer;
|
|
1011
|
+
left: 14px;
|
|
1012
|
+
transition: transform 0.15s, box-shadow 0.15s;
|
|
1013
|
+
display: flex;
|
|
1014
|
+
align-items: center;
|
|
1015
|
+
justify-content: center;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
.margin-dot:hover {
|
|
1019
|
+
transform: scale(1.3);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.margin-dot--open {
|
|
1023
|
+
background: var(--color-primary);
|
|
1024
|
+
box-shadow: 0 0 6px rgba(37, 99, 235, 0.3);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.margin-dot--resolved {
|
|
1028
|
+
background: var(--color-text-muted);
|
|
1029
|
+
opacity: 0.4;
|
|
1030
|
+
display: none;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.plan-layout--show-resolved .margin-dot--resolved {
|
|
1034
|
+
display: flex;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/* Ensure content doesn't get hidden behind fixed toolbar */
|
|
1038
|
+
body:has(.comment-toolbar) .main-content {
|
|
1039
|
+
padding-bottom: 3.5rem;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/* Comment toolbar (bottom-pinned) */
|
|
1043
|
+
.comment-toolbar {
|
|
1044
|
+
position: fixed;
|
|
1045
|
+
bottom: 0;
|
|
1046
|
+
left: 0;
|
|
1047
|
+
right: 0;
|
|
1048
|
+
background: var(--color-surface);
|
|
1049
|
+
border-top: 1px solid var(--color-border);
|
|
1050
|
+
padding: var(--space-sm) var(--space-lg);
|
|
1051
|
+
display: flex;
|
|
1052
|
+
align-items: center;
|
|
1053
|
+
gap: var(--space-lg);
|
|
1054
|
+
z-index: 50;
|
|
1055
|
+
justify-content: center;
|
|
1056
|
+
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.comment-toolbar__count {
|
|
1060
|
+
font-size: var(--text-sm);
|
|
1061
|
+
font-weight: 600;
|
|
1062
|
+
color: var(--color-text);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
.comment-toolbar__nav {
|
|
1066
|
+
display: flex;
|
|
1067
|
+
align-items: center;
|
|
1068
|
+
gap: var(--space-xs);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.comment-toolbar__position {
|
|
1072
|
+
font-size: var(--text-sm);
|
|
1073
|
+
color: var(--color-text-muted);
|
|
1074
|
+
min-width: 4rem;
|
|
1075
|
+
text-align: center;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.comment-toolbar__toggle {
|
|
1079
|
+
font-size: var(--text-sm);
|
|
1080
|
+
color: var(--color-text-muted);
|
|
1081
|
+
cursor: pointer;
|
|
1082
|
+
display: flex;
|
|
1083
|
+
align-items: center;
|
|
1084
|
+
gap: var(--space-xs);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
.comment-toolbar__toggle input[type="checkbox"] {
|
|
1088
|
+
cursor: pointer;
|
|
1089
|
+
}
|
|
@@ -56,7 +56,7 @@ module CoPlan
|
|
|
56
56
|
render json: { thread_id: thread.id, status: thread.status }
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
def
|
|
59
|
+
def discard
|
|
60
60
|
thread = @plan.comment_threads.find_by(id: params[:id])
|
|
61
61
|
unless thread
|
|
62
62
|
render json: { error: "Comment thread not found" }, status: :not_found
|
|
@@ -64,12 +64,12 @@ module CoPlan
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
policy = CommentThreadPolicy.new(current_user, thread)
|
|
67
|
-
unless policy.
|
|
67
|
+
unless policy.discard?
|
|
68
68
|
render json: { error: "Not authorized" }, status: :forbidden
|
|
69
69
|
return
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
thread.
|
|
72
|
+
thread.discard!(current_user)
|
|
73
73
|
broadcast_thread_update(thread)
|
|
74
74
|
|
|
75
75
|
render json: { thread_id: thread.id, status: thread.status }
|
|
@@ -104,10 +104,10 @@ module CoPlan
|
|
|
104
104
|
private
|
|
105
105
|
|
|
106
106
|
def broadcast_new_thread(thread)
|
|
107
|
-
Broadcaster.
|
|
107
|
+
Broadcaster.append_to(
|
|
108
108
|
@plan,
|
|
109
|
-
target: "
|
|
110
|
-
partial: "coplan/comment_threads/
|
|
109
|
+
target: "plan-threads",
|
|
110
|
+
partial: "coplan/comment_threads/thread_popover",
|
|
111
111
|
locals: { thread: thread, plan: @plan }
|
|
112
112
|
)
|
|
113
113
|
end
|
|
@@ -116,7 +116,7 @@ module CoPlan
|
|
|
116
116
|
Broadcaster.replace_to(
|
|
117
117
|
@plan,
|
|
118
118
|
target: ActionView::RecordIdentifier.dom_id(thread),
|
|
119
|
-
partial: "coplan/comment_threads/
|
|
119
|
+
partial: "coplan/comment_threads/thread_popover",
|
|
120
120
|
locals: { thread: thread, plan: @plan }
|
|
121
121
|
)
|
|
122
122
|
end
|
|
@@ -3,11 +3,15 @@ module CoPlan
|
|
|
3
3
|
include ActionView::RecordIdentifier
|
|
4
4
|
|
|
5
5
|
before_action :set_plan
|
|
6
|
-
before_action :set_thread, only: [:resolve, :accept, :
|
|
6
|
+
before_action :set_thread, only: [:resolve, :accept, :discard, :reopen]
|
|
7
7
|
|
|
8
8
|
def create
|
|
9
9
|
authorize!(@plan, :show?)
|
|
10
10
|
|
|
11
|
+
# Author's own comments start as "todo" (self-assigned work item);
|
|
12
|
+
# non-author comments start as "pending" (awaiting author triage).
|
|
13
|
+
initial_status = current_user.id == @plan.created_by_user_id ? "todo" : "pending"
|
|
14
|
+
|
|
11
15
|
thread = @plan.comment_threads.new(
|
|
12
16
|
plan_version: @plan.current_plan_version,
|
|
13
17
|
anchor_text: params[:comment_thread][:anchor_text].presence,
|
|
@@ -15,7 +19,8 @@ module CoPlan
|
|
|
15
19
|
anchor_occurrence: params[:comment_thread][:anchor_occurrence].presence&.to_i,
|
|
16
20
|
start_line: params[:comment_thread][:start_line].presence,
|
|
17
21
|
end_line: params[:comment_thread][:end_line].presence,
|
|
18
|
-
created_by_user: current_user
|
|
22
|
+
created_by_user: current_user,
|
|
23
|
+
status: initial_status
|
|
19
24
|
)
|
|
20
25
|
|
|
21
26
|
thread.save!
|
|
@@ -26,8 +31,14 @@ module CoPlan
|
|
|
26
31
|
body_markdown: params[:comment_thread][:body_markdown]
|
|
27
32
|
)
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
if thread.anchored?
|
|
35
|
+
Broadcaster.append_to(
|
|
36
|
+
@plan,
|
|
37
|
+
target: "plan-threads",
|
|
38
|
+
partial: "coplan/comment_threads/thread_popover",
|
|
39
|
+
locals: { thread: thread, plan: @plan }
|
|
40
|
+
)
|
|
41
|
+
end
|
|
31
42
|
|
|
32
43
|
respond_with_stream_or_redirect("Comment added.")
|
|
33
44
|
end
|
|
@@ -35,34 +46,28 @@ module CoPlan
|
|
|
35
46
|
def resolve
|
|
36
47
|
authorize!(@thread, :resolve?)
|
|
37
48
|
@thread.resolve!(current_user)
|
|
38
|
-
|
|
49
|
+
broadcast_thread_replace(@thread)
|
|
39
50
|
respond_with_stream_or_redirect("Thread resolved.")
|
|
40
51
|
end
|
|
41
52
|
|
|
42
53
|
def accept
|
|
43
54
|
authorize!(@thread, :accept?)
|
|
44
55
|
@thread.accept!(current_user)
|
|
45
|
-
|
|
56
|
+
broadcast_thread_replace(@thread)
|
|
46
57
|
respond_with_stream_or_redirect("Thread accepted.")
|
|
47
58
|
end
|
|
48
59
|
|
|
49
|
-
def
|
|
50
|
-
authorize!(@thread, :
|
|
51
|
-
@thread.
|
|
52
|
-
|
|
53
|
-
respond_with_stream_or_redirect("Thread
|
|
60
|
+
def discard
|
|
61
|
+
authorize!(@thread, :discard?)
|
|
62
|
+
@thread.discard!(current_user)
|
|
63
|
+
broadcast_thread_replace(@thread)
|
|
64
|
+
respond_with_stream_or_redirect("Thread discarded.")
|
|
54
65
|
end
|
|
55
66
|
|
|
56
67
|
def reopen
|
|
57
68
|
authorize!(@thread, :reopen?)
|
|
58
|
-
@thread.update!(status: "
|
|
59
|
-
|
|
60
|
-
# since the active scope excludes out_of_date rows.
|
|
61
|
-
if @thread.out_of_date?
|
|
62
|
-
broadcast_thread_replace(@thread)
|
|
63
|
-
else
|
|
64
|
-
broadcast_thread_move(@thread, from: "resolved-comment-threads", to: "comment-threads")
|
|
65
|
-
end
|
|
69
|
+
@thread.update!(status: "pending", resolved_by_user: nil)
|
|
70
|
+
broadcast_thread_replace(@thread)
|
|
66
71
|
respond_with_stream_or_redirect("Thread reopened.")
|
|
67
72
|
end
|
|
68
73
|
|
|
@@ -76,15 +81,6 @@ module CoPlan
|
|
|
76
81
|
@thread = @plan.comment_threads.find(params[:id])
|
|
77
82
|
end
|
|
78
83
|
|
|
79
|
-
def broadcast_new_thread(thread)
|
|
80
|
-
Broadcaster.prepend_to(
|
|
81
|
-
@plan,
|
|
82
|
-
target: "comment-threads",
|
|
83
|
-
partial: "coplan/comment_threads/thread",
|
|
84
|
-
locals: { thread: thread, plan: @plan }
|
|
85
|
-
)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
84
|
# Broadcasts update all clients (including the submitter) via WebSocket.
|
|
89
85
|
# The empty turbo_stream response prevents Turbo from navigating (which causes scroll-to-top).
|
|
90
86
|
def respond_with_stream_or_redirect(message)
|
|
@@ -94,56 +90,14 @@ module CoPlan
|
|
|
94
90
|
end
|
|
95
91
|
end
|
|
96
92
|
|
|
97
|
-
# Replaces a thread in place (status changed
|
|
93
|
+
# Replaces a thread in place (status changed).
|
|
98
94
|
def broadcast_thread_replace(thread)
|
|
99
95
|
Broadcaster.replace_to(
|
|
100
96
|
@plan,
|
|
101
97
|
target: dom_id(thread),
|
|
102
|
-
partial: "coplan/comment_threads/
|
|
98
|
+
partial: "coplan/comment_threads/thread_popover",
|
|
103
99
|
locals: { thread: thread, plan: @plan }
|
|
104
100
|
)
|
|
105
|
-
broadcast_tab_counts
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Moves a thread between Open/Resolved lists and updates tab counts.
|
|
109
|
-
def broadcast_thread_move(thread, from:, to:)
|
|
110
|
-
Broadcaster.remove_to(@plan, target: dom_id(thread))
|
|
111
|
-
Broadcaster.append_to(
|
|
112
|
-
@plan,
|
|
113
|
-
target: to,
|
|
114
|
-
partial: "coplan/comment_threads/thread",
|
|
115
|
-
locals: { thread: thread, plan: @plan }
|
|
116
|
-
)
|
|
117
|
-
broadcast_tab_counts
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def broadcast_tab_counts
|
|
121
|
-
threads = @plan.comment_threads
|
|
122
|
-
open_count = threads.active.count
|
|
123
|
-
resolved_count = threads.archived.count
|
|
124
|
-
|
|
125
|
-
Broadcaster.update_to(
|
|
126
|
-
@plan,
|
|
127
|
-
target: "open-thread-count",
|
|
128
|
-
html: open_count > 0 ? open_count.to_s : ""
|
|
129
|
-
)
|
|
130
|
-
Broadcaster.update_to(
|
|
131
|
-
@plan,
|
|
132
|
-
target: "resolved-thread-count",
|
|
133
|
-
html: resolved_count > 0 ? resolved_count.to_s : ""
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
# Toggle empty-state placeholders
|
|
137
|
-
Broadcaster.replace_to(
|
|
138
|
-
@plan,
|
|
139
|
-
target: "open-threads-empty",
|
|
140
|
-
html: %(<p class="text-sm text-muted" id="open-threads-empty" #{'style="display: none;"' if open_count > 0}>No open comments.</p>)
|
|
141
|
-
)
|
|
142
|
-
Broadcaster.replace_to(
|
|
143
|
-
@plan,
|
|
144
|
-
target: "resolved-threads-empty",
|
|
145
|
-
html: %(<p class="text-sm text-muted" id="resolved-threads-empty" #{'style="display: none;"' if resolved_count > 0}>No resolved comments.</p>)
|
|
146
|
-
)
|
|
147
101
|
end
|
|
148
102
|
end
|
|
149
103
|
end
|
|
@@ -10,9 +10,7 @@ module CoPlan
|
|
|
10
10
|
|
|
11
11
|
def show
|
|
12
12
|
authorize!(@plan, :show?)
|
|
13
|
-
threads = @plan.comment_threads.includes(:comments, :created_by_user
|
|
14
|
-
@active_threads = threads.active
|
|
15
|
-
@archived_threads = threads.archived
|
|
13
|
+
@threads = @plan.comment_threads.includes(:comments, :created_by_user).order(:created_at)
|
|
16
14
|
end
|
|
17
15
|
|
|
18
16
|
def edit
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
submitOnEnter(event) {
|
|
5
|
+
if (event.key !== "Enter") return
|
|
6
|
+
if (event.shiftKey || event.isComposing) return
|
|
7
|
+
|
|
8
|
+
const form = event.target.closest("form")
|
|
9
|
+
if (!form) return
|
|
10
|
+
|
|
11
|
+
event.preventDefault()
|
|
12
|
+
form.requestSubmit()
|
|
13
|
+
}
|
|
14
|
+
}
|