collavre 0.5.0 → 0.6.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/comment_versions.css +76 -0
  3. data/app/assets/stylesheets/collavre/comments_popup.css +190 -36
  4. data/app/assets/stylesheets/collavre/popup.css +3 -1
  5. data/app/controllers/collavre/comments/versions_controller.rb +82 -0
  6. data/app/controllers/collavre/comments_controller.rb +40 -6
  7. data/app/javascript/controllers/comment_controller.js +33 -70
  8. data/app/javascript/controllers/comment_version_controller.js +164 -0
  9. data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +305 -0
  10. data/app/javascript/controllers/comments/__tests__/list_controller_selection.test.js +103 -0
  11. data/app/javascript/controllers/comments/__tests__/review_quotes_store.test.js +113 -0
  12. data/app/javascript/controllers/comments/form_controller.js +276 -12
  13. data/app/javascript/controllers/comments/list_controller.js +146 -62
  14. data/app/javascript/controllers/comments/review_quotes_store.js +189 -0
  15. data/app/javascript/controllers/index.js +7 -1
  16. data/app/javascript/controllers/topic_search_controller.js +103 -0
  17. data/app/jobs/collavre/ai_agent_job.rb +46 -4
  18. data/app/jobs/collavre/compress_job.rb +92 -0
  19. data/app/models/collavre/comment.rb +35 -1
  20. data/app/models/collavre/comment_version.rb +15 -0
  21. data/app/models/collavre/task.rb +30 -2
  22. data/app/models/collavre/user.rb +8 -0
  23. data/app/services/collavre/ai_agent/review_handler.rb +18 -1
  24. data/app/services/collavre/ai_client.rb +6 -0
  25. data/app/services/collavre/command_menu_service.rb +12 -0
  26. data/app/services/collavre/comments/command_processor.rb +3 -1
  27. data/app/services/collavre/comments/compress_command.rb +75 -0
  28. data/app/services/collavre/comments/work_command.rb +175 -0
  29. data/app/services/collavre/comments/workflow_executor.rb +344 -0
  30. data/app/services/collavre/creatives/tree_formatter.rb +53 -13
  31. data/app/services/collavre/gemini_parent_recommender.rb +3 -3
  32. data/app/services/collavre/orchestration/agent_orchestrator.rb +15 -4
  33. data/app/services/collavre/orchestration/scheduler.rb +3 -2
  34. data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
  35. data/app/services/collavre/system_events/dispatcher.rb +9 -0
  36. data/app/services/collavre/tools/creative_create_service.rb +1 -8
  37. data/app/services/collavre/tools/creative_import_service.rb +46 -0
  38. data/app/services/collavre/tools/creative_retrieval_service.rb +156 -96
  39. data/app/services/collavre/tools/creative_update_service.rb +1 -8
  40. data/app/services/collavre/tools/description_normalizable.rb +16 -0
  41. data/app/views/collavre/comments/_comment.html.erb +25 -8
  42. data/app/views/collavre/comments/_comments_popup.html.erb +20 -4
  43. data/config/locales/comments.en.yml +53 -2
  44. data/config/locales/comments.ko.yml +53 -2
  45. data/config/routes.rb +6 -0
  46. data/db/migrate/20260220072200_add_workflow_fields_to_tasks.rb +12 -0
  47. data/db/migrate/20260223173533_add_review_type_to_comments.rb +5 -0
  48. data/db/migrate/20260225065200_create_comment_versions.rb +14 -0
  49. data/db/migrate/20260225074416_add_selected_version_id_to_comments.rb +7 -0
  50. data/lib/collavre/version.rb +1 -1
  51. metadata +20 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fa2404d51d4e3c83b1c475d2ceb48d2bc834193f825e25cfcb15f5d8dc1a3c7
4
- data.tar.gz: 9139d284ca5dd900d79a048b299995558cfbc5dc6acf7324416fac394aa60982
3
+ metadata.gz: 88ee0fe394fdf6b52cb501be92fbac6b05e549adf30aa46937d7e9a14a690e7c
4
+ data.tar.gz: e3573f4001310e3227e6084f5544af98412ba341d217cec06731974d57fc154d
5
5
  SHA512:
6
- metadata.gz: 25d88e2068d1810640e9325ed5bcac9d1a07e8452f7843bb646b4d005314bd664f5b0247dbf92036954e688d98f12e9aef65e1ece13d841e07954619e2483339
7
- data.tar.gz: 531f7331c316c8d8f1847bbbd6b0dfd386022a968fbdcbf9c0564b1d34b16599fa450841a140bf0619b12df683c3bc8a3cecb8d0b691a242aca3b5efbfcffcb5
6
+ metadata.gz: a139147e03c807d07ab876d053172acfb736956fb5d6a8e1d519a070b83913caba150389153329461963129394b52af20cbd53b7e0506a68f5c4210dc6625cc2
7
+ data.tar.gz: f9f2ea3a2921c3baee34452e8908cd48e93b59692124b54b672312255eb8a25eb77f8b3dad61f3ce17e6533f9d79e7b11ce7bd928dfcc74ea336dc30329204b0
@@ -0,0 +1,76 @@
1
+ .comment-version-navigator {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--space-2, 0.25rem);
5
+ margin-top: var(--space-2, 0.25rem);
6
+ font-size: var(--text-xs, 0.75rem);
7
+ }
8
+
9
+ .comment-version-btn {
10
+ background: none;
11
+ border: 1px solid var(--border-color, #ddd);
12
+ border-radius: var(--radius-1, 4px);
13
+ cursor: pointer;
14
+ padding: 0 var(--space-2, 0.25rem);
15
+ font-size: var(--text-xs, 0.75rem);
16
+ line-height: 1.6;
17
+ color: var(--text-color, #333);
18
+ }
19
+
20
+ .comment-version-btn:hover:not(:disabled) {
21
+ background: var(--surface-hover, #f0f0f0);
22
+ }
23
+
24
+ .comment-version-btn:disabled {
25
+ opacity: 0.3;
26
+ cursor: default;
27
+ }
28
+
29
+ .comment-version-indicator {
30
+ color: var(--text-muted, #888);
31
+ font-variant-numeric: tabular-nums;
32
+ min-width: 3em;
33
+ text-align: center;
34
+ }
35
+
36
+ .comment-version-delete-btn {
37
+ background: none;
38
+ border: none;
39
+ cursor: pointer;
40
+ color: var(--text-muted, #888);
41
+ font-size: var(--text-xs, 0.75rem);
42
+ padding: 0 var(--space-1, 0.125rem);
43
+ margin-left: var(--space-1, 0.125rem);
44
+ }
45
+
46
+ .comment-version-delete-btn:hover {
47
+ color: var(--danger-color, #e53e3e);
48
+ }
49
+
50
+ .comment-version-selected {
51
+ color: var(--primary-color, #4a90d9);
52
+ font-weight: bold;
53
+ }
54
+
55
+ .comment-version-select-btn {
56
+ background: none;
57
+ border: 1px solid var(--primary-color, #4a90d9);
58
+ border-radius: var(--radius-1, 4px);
59
+ cursor: pointer;
60
+ color: var(--primary-color, #4a90d9);
61
+ font-size: var(--text-xs, 0.75rem);
62
+ padding: 0 var(--space-2, 0.25rem);
63
+ margin-left: var(--space-1, 0.125rem);
64
+ line-height: 1.6;
65
+ }
66
+
67
+ .comment-version-select-btn:hover:not(:disabled) {
68
+ background: var(--primary-color, #4a90d9);
69
+ color: white;
70
+ }
71
+
72
+ .comment-version-select-btn:disabled,
73
+ .comment-version-delete-btn:disabled {
74
+ opacity: 0.3;
75
+ cursor: default;
76
+ }
@@ -138,8 +138,12 @@ body.chat-fullscreen {
138
138
 
139
139
  #new-comment-form textarea {
140
140
  width: 96%;
141
- resize: vertical;
141
+ resize: none;
142
142
  font-size: var(--text-1);
143
+ overflow-y: auto;
144
+ min-height: calc(var(--text-1) * 1.5 * 2); /* 2 lines minimum */
145
+ max-height: calc(var(--text-1) * 1.5 * 10); /* 10 lines maximum */
146
+ transition: height 0.1s ease-out;
143
147
  /* Prevent iOS zoom on focus */
144
148
  }
145
149
 
@@ -196,10 +200,11 @@ body.chat-fullscreen {
196
200
  margin-bottom: 0.2em;
197
201
  }
198
202
 
199
- .comment-quoted-text {
203
+ .comment-quoted-text,
204
+ .comment-content blockquote {
200
205
  border-left: 3px solid var(--color-border);
201
- padding: 0.3em 0.6em;
202
- margin: 0;
206
+ padding: 0.2em 0.6em;
207
+ margin: 0 0 0.3em 0;
203
208
  color: var(--color-muted);
204
209
  font-size: 0.9em;
205
210
  background: color-mix(in srgb, var(--color-section-bg) 50%, transparent);
@@ -208,6 +213,10 @@ body.chat-fullscreen {
208
213
  word-break: break-word;
209
214
  }
210
215
 
216
+ .comment-content blockquote p {
217
+ margin: 0;
218
+ }
219
+
211
220
  /* Quote indicator in form */
212
221
  .comment-quote-indicator {
213
222
  display: flex;
@@ -245,6 +254,100 @@ body.chat-fullscreen {
245
254
  color: var(--color-text);
246
255
  }
247
256
 
257
+ /* Review quote chips */
258
+ .review-quotes-container {
259
+ display: flex;
260
+ flex-direction: column;
261
+ gap: 0.25em;
262
+ padding: 0.4em 0.5em;
263
+ max-height: 8em;
264
+ overflow-y: auto;
265
+ }
266
+
267
+ .review-quote-chip {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 0.3em;
271
+ padding: 0.25em 0.5em;
272
+ background: color-mix(in srgb, var(--color-section-bg) 50%, transparent);
273
+ border-left: 3px solid var(--color-muted);
274
+ border-radius: 0 var(--radius-2) var(--radius-2) 0;
275
+ font-size: var(--text-0);
276
+ color: var(--color-muted);
277
+ line-height: 1.3;
278
+ transition: border-color 0.15s ease, background 0.15s ease;
279
+ }
280
+
281
+ .review-quote-chip--active {
282
+ border-left-color: var(--color-active);
283
+ background: color-mix(in srgb, var(--color-active) 8%, transparent);
284
+ }
285
+
286
+ .review-quote-type-toggle {
287
+ border: none;
288
+ background: none;
289
+ cursor: pointer;
290
+ padding: 0 0.1em;
291
+ font-size: 0.85em;
292
+ line-height: 1;
293
+ flex-shrink: 0;
294
+ opacity: 0.8;
295
+ transition: opacity 0.15s ease;
296
+ }
297
+
298
+ .review-quote-type-toggle:hover {
299
+ opacity: 1;
300
+ }
301
+
302
+ .review-quote-chip-text {
303
+ flex: 1;
304
+ min-width: 0;
305
+ overflow: hidden;
306
+ text-overflow: ellipsis;
307
+ white-space: nowrap;
308
+ cursor: pointer;
309
+ }
310
+
311
+ .review-quote-chip-text:hover {
312
+ color: var(--color-text);
313
+ }
314
+
315
+ .review-quote-chip-feedback {
316
+ flex-shrink: 1;
317
+ min-width: 0;
318
+ overflow: hidden;
319
+ text-overflow: ellipsis;
320
+ white-space: nowrap;
321
+ font-size: 0.85em;
322
+ color: var(--color-text);
323
+ opacity: 0.7;
324
+ }
325
+
326
+ .review-quote-chip-remove {
327
+ border: none;
328
+ background: none;
329
+ cursor: pointer;
330
+ color: var(--color-muted);
331
+ font-size: 1em;
332
+ padding: 0 0.15em;
333
+ line-height: 1;
334
+ flex-shrink: 0;
335
+ }
336
+
337
+ .review-quote-chip-remove:hover {
338
+ color: var(--color-danger);
339
+ }
340
+
341
+ .review-submit-btn {
342
+ font-size: var(--text-0) !important;
343
+ white-space: nowrap;
344
+ }
345
+
346
+ .comment-highlight {
347
+ background: color-mix(in srgb, var(--color-active) 15%, transparent) !important;
348
+ transition: background 0.3s ease;
349
+ }
350
+
248
351
  /* Review popup (uses common-popup) */
249
352
  .comment-review-popup {
250
353
  min-width: auto;
@@ -599,7 +702,6 @@ body.chat-fullscreen {
599
702
  .delete-comment-btn,
600
703
  .edit-comment-btn,
601
704
  .review-comment-btn,
602
- .replace-comment-btn,
603
705
  .copy-comment-link-btn {
604
706
  border: none;
605
707
  background: none;
@@ -614,11 +716,6 @@ body.chat-fullscreen {
614
716
  display: none;
615
717
  }
616
718
 
617
- .replace-comment-btn:disabled {
618
- opacity: 0.35;
619
- cursor: default;
620
- }
621
-
622
719
  .comment-copy-notice {
623
720
  position: absolute;
624
721
  right: 0.2em;
@@ -1022,35 +1119,81 @@ body.chat-fullscreen {
1022
1119
  outline: none;
1023
1120
  }
1024
1121
 
1025
- /* Selection hint popup */
1026
- .selection-hint-popup {
1027
- position: fixed;
1122
+ /* Selection action bar */
1123
+ .selection-action-bar {
1124
+ position: sticky;
1125
+ bottom: 0;
1028
1126
  background: var(--surface-section);
1029
- border: 1px solid var(--border-color);
1030
- border-radius: 8px;
1031
- padding: var(--space-2) 12px;
1032
- box-shadow: var(--shadow-2);
1033
- white-space: nowrap;
1034
- z-index: 10000;
1035
- animation: hint-fade-in 0.2s ease-out;
1127
+ border-top: 1px solid var(--border-color);
1128
+ padding: var(--space-2) var(--space-3);
1129
+ z-index: 100;
1130
+ animation: action-bar-slide-up 0.2s ease-out;
1036
1131
  }
1037
1132
 
1038
- .selection-hint-popup .hint-content {
1133
+ .selection-action-bar-main {
1039
1134
  display: flex;
1040
- flex-direction: column;
1041
- gap: var(--space-1);
1042
- font-size: 0.8em;
1135
+ align-items: center;
1136
+ gap: var(--space-2);
1137
+ flex-wrap: wrap;
1138
+ }
1139
+
1140
+ .selection-action-bar-count {
1141
+ font-size: 0.85em;
1142
+ font-weight: bold;
1143
+ color: var(--color-text);
1144
+ margin-right: var(--space-1);
1145
+ }
1146
+
1147
+ .selection-action-bar-btn {
1148
+ border: none;
1149
+ background: none;
1150
+ cursor: pointer;
1151
+ font-size: 0.85em;
1152
+ color: var(--color-chat-btn-text);
1153
+ font-weight: bold;
1154
+ padding: var(--space-1) var(--space-2);
1155
+ border-radius: var(--radius-2);
1156
+ white-space: nowrap;
1157
+ }
1158
+
1159
+ .selection-action-bar-btn:hover {
1160
+ background: var(--surface-hover, rgba(0, 0, 0, 0.05));
1161
+ }
1162
+
1163
+ .selection-action-delete:hover {
1164
+ color: var(--color-danger);
1165
+ }
1166
+
1167
+ .selection-action-bar-close {
1168
+ border: none;
1169
+ background: none;
1170
+ cursor: pointer;
1171
+ font-size: 1em;
1043
1172
  color: var(--text-muted);
1173
+ margin-left: auto;
1174
+ padding: var(--space-1);
1044
1175
  }
1045
1176
 
1046
- .selection-hint-popup .hint-content span {
1047
- display: block;
1177
+ .selection-action-bar-close:hover {
1178
+ color: var(--color-text);
1048
1179
  }
1049
1180
 
1050
- @keyframes hint-fade-in {
1181
+ .selection-action-bar-hint {
1182
+ font-size: 0.75em;
1183
+ color: var(--text-muted);
1184
+ margin-top: var(--space-1);
1185
+ }
1186
+
1187
+ @media (pointer: coarse) {
1188
+ .selection-action-bar-hint.no-touch {
1189
+ display: none;
1190
+ }
1191
+ }
1192
+
1193
+ @keyframes action-bar-slide-up {
1051
1194
  from {
1052
1195
  opacity: 0;
1053
- transform: translateY(-4px);
1196
+ transform: translateY(8px);
1054
1197
  }
1055
1198
  to {
1056
1199
  opacity: 1;
@@ -1058,14 +1201,7 @@ body.chat-fullscreen {
1058
1201
  }
1059
1202
  }
1060
1203
 
1061
- /* Dark mode support */
1062
- @media (prefers-color-scheme: dark) {
1063
- .selection-hint-popup {
1064
- background: var(--surface-section);
1065
- border-color: var(--border-color);
1066
- }
1067
- }
1068
-
1204
+ /* Floating search popups — positioned by CommonPopup inside #comments-popup */
1069
1205
  /* Override popup-close-btn absolute positioning inside comments popup header */
1070
1206
  .comments-popup-actions .popup-close-btn {
1071
1207
  position: static;
@@ -1122,3 +1258,21 @@ body.chat-fullscreen {
1122
1258
  0%, 100% { opacity: 1; }
1123
1259
  50% { opacity: 0; }
1124
1260
  }
1261
+
1262
+ .review-hint {
1263
+ background: var(--surface-3, #333);
1264
+ color: var(--text-1, #fff);
1265
+ padding: var(--space-2, 4px) var(--space-3, 8px);
1266
+ border-radius: var(--radius-2, 4px);
1267
+ font-size: var(--text-0, 0.75rem);
1268
+ white-space: nowrap;
1269
+ z-index: var(--layer-important, 999);
1270
+ animation: review-hint-fade 3s ease-out forwards;
1271
+ pointer-events: none;
1272
+ box-shadow: var(--shadow-2, 0 2px 8px rgba(0,0,0,0.15));
1273
+ }
1274
+
1275
+ @keyframes review-hint-fade {
1276
+ 0%, 70% { opacity: 1; }
1277
+ 100% { opacity: 0; }
1278
+ }
@@ -37,8 +37,10 @@
37
37
  margin-right: 0.4em;
38
38
  }
39
39
 
40
- #link-creative-modal {
40
+ #link-creative-modal,
41
+ #topic-search-modal {
41
42
  min-width: 320px;
43
+ z-index: calc(var(--layer-modal) + 10);
42
44
  }
43
45
 
44
46
  .common-popup {
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module Comments
5
+ class VersionsController < ApplicationController
6
+ before_action :set_creative
7
+ before_action :set_comment
8
+
9
+ def index
10
+ versions = @comment.comment_versions.order(:version_number).map do |v|
11
+ {
12
+ id: v.id,
13
+ version_number: v.version_number,
14
+ content: v.content,
15
+ created_at: v.created_at.iso8601
16
+ }
17
+ end
18
+
19
+ render json: {
20
+ versions: versions,
21
+ selected_version_id: @comment.selected_version_id,
22
+ total: versions.size
23
+ }
24
+ end
25
+
26
+ def select
27
+ unless @comment.user == Current.user || @creative.has_permission?(Current.user, :admin)
28
+ render json: { error: I18n.t("collavre.comments.not_owner") }, status: :forbidden and return
29
+ end
30
+
31
+ version = @comment.comment_versions.find(params[:id])
32
+ @comment.update!(selected_version_id: version.id, content: version.content)
33
+
34
+ render json: { selected_version_id: version.id, content: version.content }
35
+ end
36
+
37
+ def destroy
38
+ unless @comment.user == Current.user || @creative.has_permission?(Current.user, :admin)
39
+ render json: { error: I18n.t("collavre.comments.not_owner") }, status: :forbidden and return
40
+ end
41
+
42
+ version = @comment.comment_versions.find(params[:id])
43
+ was_selected = @comment.selected_version_id == version.id
44
+ version.destroy!
45
+
46
+ if was_selected
47
+ # Roll back to the latest remaining version
48
+ latest = @comment.comment_versions.order(:version_number).last
49
+ if latest
50
+ @comment.update!(selected_version_id: latest.id, content: latest.content)
51
+ else
52
+ @comment.update!(selected_version_id: nil)
53
+ end
54
+ end
55
+
56
+ remaining = @comment.comment_versions.count
57
+ render json: {
58
+ selected_version_id: @comment.selected_version_id,
59
+ content: @comment.content,
60
+ total: remaining
61
+ }
62
+ end
63
+
64
+ private
65
+
66
+ def set_creative
67
+ @creative = Creative.find(params[:creative_id]).effective_origin
68
+ end
69
+
70
+ def set_comment
71
+ @comment = @creative.comments
72
+ .where(
73
+ "comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
74
+ false,
75
+ Current.user.id,
76
+ Current.user.id
77
+ )
78
+ .find(params[:comment_id])
79
+ end
80
+ end
81
+ end
82
+ end
@@ -25,7 +25,7 @@ module Collavre
25
25
  Current.user.id,
26
26
  Current.user.id
27
27
  )
28
- scope = visible_scope.with_attached_images.includes(:topic, :comment_reactions)
28
+ scope = visible_scope.with_attached_images.includes(:topic, :comment_reactions, :comment_versions)
29
29
 
30
30
  if params[:search].present?
31
31
  search_term = ActiveRecord::Base.sanitize_sql_like(params[:search].to_s.strip.downcase)
@@ -191,7 +191,7 @@ module Collavre
191
191
  )
192
192
  end
193
193
  end
194
- @comment = Comment.with_attached_images.includes(:comment_reactions).find(@comment.id)
194
+ @comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
195
195
  render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }, status: :created
196
196
  else
197
197
  render json: { errors: @comment.errors.full_messages }, status: :unprocessable_entity
@@ -206,7 +206,7 @@ module Collavre
206
206
  end
207
207
 
208
208
  if @comment.update(safe_params)
209
- @comment = Comment.with_attached_images.includes(:comment_reactions).find(@comment.id)
209
+ @comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
210
210
  render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }
211
211
  else
212
212
  render json: { errors: @comment.errors.full_messages }, status: :unprocessable_entity
@@ -295,7 +295,7 @@ module Collavre
295
295
 
296
296
  begin
297
297
  ::Comments::ActionExecutor.new(comment: @comment, executor: Current.user).call
298
- @comment = Comment.with_attached_images.includes(:comment_reactions).find(@comment.id)
298
+ @comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
299
299
  render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }
300
300
  rescue ::Comments::ActionExecutor::ExecutionError => e
301
301
  render json: { error: e.message }, status: :unprocessable_entity
@@ -365,7 +365,7 @@ module Collavre
365
365
  elsif executed_error
366
366
  render json: { error: I18n.t("collavre.comments.approve_already_executed") }, status: :unprocessable_entity
367
367
  elsif update_success
368
- @comment = Comment.with_attached_images.includes(:comment_reactions).find(@comment.id)
368
+ @comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
369
369
  render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }
370
370
  else
371
371
  error_message = @comment.errors.full_messages.to_sentence.presence || I18n.t("collavre.comments.action_update_error")
@@ -406,6 +406,40 @@ module Collavre
406
406
  render json: CommandMenuService.new(user: Current.user).items
407
407
  end
408
408
 
409
+ MAX_BATCH_DELETE = 100
410
+
411
+ def batch_destroy
412
+ comment_ids = Array(params[:comment_ids]).map(&:to_i).uniq.first(MAX_BATCH_DELETE)
413
+ if comment_ids.empty?
414
+ render json: { error: I18n.t("collavre.comments.batch_delete_no_selection") }, status: :unprocessable_entity and return
415
+ end
416
+
417
+ is_admin = @creative.has_permission?(Current.user, :admin)
418
+ is_creative_owner = @creative.user == Current.user
419
+
420
+ visible_scope = @creative.comments.where(
421
+ "comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
422
+ false, Current.user.id, Current.user.id
423
+ )
424
+ comments = visible_scope.where(id: comment_ids).to_a
425
+
426
+ if comments.length != comment_ids.length
427
+ render json: { error: I18n.t("collavre.comments.batch_delete_not_found") }, status: :not_found and return
428
+ end
429
+
430
+ # Check permissions: user must own all comments, or be admin/creative owner
431
+ unless is_admin || is_creative_owner
432
+ unauthorized = comments.reject { |c| c.user == Current.user }
433
+ if unauthorized.any?
434
+ render json: { error: I18n.t("collavre.comments.not_owner") }, status: :forbidden and return
435
+ end
436
+ end
437
+
438
+ Comment.where(id: comments.map(&:id)).destroy_all
439
+
440
+ head :no_content
441
+ end
442
+
409
443
  def move
410
444
  result = CommentMoveService.new(creative: @creative, user: Current.user).call(
411
445
  comment_ids: params[:comment_ids],
@@ -438,7 +472,7 @@ module Collavre
438
472
  end
439
473
 
440
474
  def comment_params
441
- params.require(:comment).permit(:content, :private, :topic_id, :quoted_comment_id, :quoted_text, images: [])
475
+ params.require(:comment).permit(:content, :private, :topic_id, :quoted_comment_id, :quoted_text, :review_type, images: [])
442
476
  end
443
477
 
444
478
  def can_convert_comment?