collavre 0.8.2 → 0.9.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/collavre/activity_logs.css +1 -1
- data/app/assets/stylesheets/collavre/comment_versions.css +10 -6
- data/app/assets/stylesheets/collavre/comments_popup.css +60 -2
- data/app/assets/stylesheets/collavre/creatives.css +7 -4
- data/app/assets/stylesheets/collavre/dark_mode.css +25 -5
- data/app/controllers/collavre/comments_controller.rb +2 -1
- data/app/controllers/collavre/topics_controller.rb +80 -3
- data/app/controllers/concerns/collavre/comments/approval_actions.rb +5 -0
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +32 -0
- data/app/javascript/controllers/comment_version_controller.js +5 -1
- data/app/javascript/controllers/comments/contexts_controller.js +118 -3
- data/app/javascript/controllers/comments/list_controller.js +67 -1
- data/app/javascript/controllers/comments/popup_controller.js +143 -10
- data/app/javascript/controllers/comments/presence_controller.js +22 -0
- data/app/javascript/controllers/comments/topics_controller.js +105 -6
- data/app/javascript/controllers/creatives/expansion_controller.js +8 -0
- data/app/javascript/controllers/creatives/select_mode_controller.js +4 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +5 -5
- data/app/javascript/modules/creative_row_editor.js +13 -0
- data/app/jobs/collavre/compress_job.rb +24 -25
- data/app/jobs/collavre/merge_comments_job.rb +79 -0
- data/app/models/collavre/topic.rb +29 -0
- data/app/models/concerns/collavre/ai_agent_resolvable.rb +42 -0
- data/app/services/collavre/comments/topic_command.rb +27 -13
- data/app/views/collavre/comments/_comments_popup.html.erb +5 -1
- data/config/locales/comments.en.yml +11 -1
- data/config/locales/comments.ko.yml +11 -1
- data/config/routes.rb +2 -0
- data/lib/collavre/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 656a876df08e2034398807dd976a0da67f7b4e5120dd73301ea521dd1291eb8b
|
|
4
|
+
data.tar.gz: c17a5821354531dfaaafd6dd51794114a132c42a3535d612410a838d75499050
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9547afa6f502aa818ef3a565d3e8991f25298623dffd980a2d63f3383debd2eef08ce0440285d5fe7c1f8dc7810245cb1aea8f252bf44d8cdd1400460b280fa1
|
|
7
|
+
data.tar.gz: 336344900a57853600abb3d8f879741d52f8673238a63b90b5054fe5d5f8b412fb54aced24dd9c93ea07a1c66175fe5518a58bd6011f9aae363a0a82720e77ef
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
padding: 0 var(--space-2, 0.25rem);
|
|
15
15
|
font-size: var(--text-xs, 0.75rem);
|
|
16
16
|
line-height: 1.6;
|
|
17
|
-
color: var(--text-
|
|
17
|
+
color: var(--text-primary, #202123);
|
|
18
|
+
font-weight: bold;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
.comment-version-btn:hover:not(:disabled) {
|
|
@@ -35,16 +36,18 @@
|
|
|
35
36
|
|
|
36
37
|
.comment-version-delete-btn {
|
|
37
38
|
background: none;
|
|
38
|
-
border:
|
|
39
|
+
border: 1px solid var(--border-color, #ddd);
|
|
40
|
+
border-radius: var(--radius-1, 4px);
|
|
39
41
|
cursor: pointer;
|
|
40
|
-
color: var(--text-
|
|
42
|
+
color: var(--text-primary, #202123);
|
|
41
43
|
font-size: var(--text-xs, 0.75rem);
|
|
42
|
-
padding: 0 var(--space-
|
|
44
|
+
padding: 0 var(--space-2, 0.25rem);
|
|
43
45
|
margin-left: var(--space-1, 0.125rem);
|
|
46
|
+
line-height: 1.6;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
.comment-version-delete-btn:hover {
|
|
47
|
-
|
|
49
|
+
.comment-version-delete-btn:hover:not(:disabled) {
|
|
50
|
+
background: var(--surface-hover, #f0f0f0);
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
.comment-version-selected {
|
|
@@ -59,6 +62,7 @@
|
|
|
59
62
|
cursor: pointer;
|
|
60
63
|
color: var(--primary-color, #4a90d9);
|
|
61
64
|
font-size: var(--text-xs, 0.75rem);
|
|
65
|
+
font-weight: bold;
|
|
62
66
|
padding: 0 var(--space-2, 0.25rem);
|
|
63
67
|
margin-left: var(--space-1, 0.125rem);
|
|
64
68
|
line-height: 1.6;
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
min-width: 200px;
|
|
11
11
|
min-height: 200px;
|
|
12
12
|
background: var(--surface-bg);
|
|
13
|
-
border:
|
|
13
|
+
border: var(--border-1) solid var(--color-active);
|
|
14
14
|
border-radius: var(--radius-3);
|
|
15
|
-
box-shadow: var(--shadow-3)
|
|
15
|
+
box-shadow: var(--shadow-3),
|
|
16
|
+
0 0 6px color-mix(in srgb, var(--color-active) 25%, transparent),
|
|
17
|
+
0 0 14px color-mix(in srgb, var(--color-active) 12%, transparent);
|
|
16
18
|
padding: 1em;
|
|
17
19
|
flex-direction: column;
|
|
18
20
|
transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease,
|
|
@@ -982,6 +984,12 @@ body.chat-fullscreen {
|
|
|
982
984
|
margin-right: -1px;
|
|
983
985
|
}
|
|
984
986
|
|
|
987
|
+
.comment-contexts-list.context-drop-active {
|
|
988
|
+
outline: 2px dashed var(--color-link);
|
|
989
|
+
outline-offset: -2px;
|
|
990
|
+
background-color: color-mix(in srgb, var(--color-link) 8%, transparent);
|
|
991
|
+
}
|
|
992
|
+
|
|
985
993
|
.context-chip[draggable="true"] {
|
|
986
994
|
cursor: grab;
|
|
987
995
|
}
|
|
@@ -1098,6 +1106,30 @@ body.chat-fullscreen {
|
|
|
1098
1106
|
box-sizing: border-box;
|
|
1099
1107
|
}
|
|
1100
1108
|
|
|
1109
|
+
.topic-agent-avatar {
|
|
1110
|
+
width: 16px;
|
|
1111
|
+
height: 16px;
|
|
1112
|
+
border-radius: 50%;
|
|
1113
|
+
object-fit: cover;
|
|
1114
|
+
flex-shrink: 0;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
.ai-agent-draggable {
|
|
1118
|
+
cursor: grab;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
.ai-agent-draggable:active,
|
|
1122
|
+
.ai-agent-draggable.dragging {
|
|
1123
|
+
cursor: grabbing;
|
|
1124
|
+
opacity: 0.5;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.topic-creation-container.drag-over {
|
|
1128
|
+
background: var(--color-active);
|
|
1129
|
+
border-radius: 12px;
|
|
1130
|
+
opacity: 0.8;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1101
1133
|
.topic-tag:hover {
|
|
1102
1134
|
background: var(--surface-hover);
|
|
1103
1135
|
}
|
|
@@ -1342,6 +1374,23 @@ body.chat-fullscreen {
|
|
|
1342
1374
|
flex-wrap: wrap;
|
|
1343
1375
|
}
|
|
1344
1376
|
|
|
1377
|
+
.selection-action-bar-select-all {
|
|
1378
|
+
display: flex;
|
|
1379
|
+
align-items: center;
|
|
1380
|
+
gap: var(--space-1);
|
|
1381
|
+
font-size: 0.85em;
|
|
1382
|
+
font-weight: bold;
|
|
1383
|
+
color: var(--color-text);
|
|
1384
|
+
cursor: pointer;
|
|
1385
|
+
user-select: none;
|
|
1386
|
+
white-space: nowrap;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
.selection-action-bar-select-all-checkbox {
|
|
1390
|
+
cursor: pointer;
|
|
1391
|
+
margin: 0;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1345
1394
|
.selection-action-bar-count {
|
|
1346
1395
|
font-size: 0.85em;
|
|
1347
1396
|
font-weight: bold;
|
|
@@ -1365,6 +1414,15 @@ body.chat-fullscreen {
|
|
|
1365
1414
|
background: var(--surface-hover);
|
|
1366
1415
|
}
|
|
1367
1416
|
|
|
1417
|
+
.selection-action-bar-btn:disabled {
|
|
1418
|
+
opacity: 0.4;
|
|
1419
|
+
cursor: not-allowed;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
.selection-action-bar-btn:disabled:hover {
|
|
1423
|
+
background: none;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1368
1426
|
.selection-action-delete:hover {
|
|
1369
1427
|
color: var(--color-danger);
|
|
1370
1428
|
}
|
|
@@ -164,10 +164,6 @@ creative-tree-row.show-edit .creative-row {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
.creative-row:hover .creative-content {
|
|
167
|
-
text-decoration: underline;
|
|
168
|
-
text-decoration-color: var(--color-link);
|
|
169
|
-
text-decoration-thickness: 2px;
|
|
170
|
-
text-underline-offset: 2px;
|
|
171
167
|
cursor: pointer;
|
|
172
168
|
}
|
|
173
169
|
|
|
@@ -440,6 +436,13 @@ creative-tree-row:not([expanded]) .creative-toggle-btn {
|
|
|
440
436
|
background-color: var(--border-drag-over);
|
|
441
437
|
}
|
|
442
438
|
|
|
439
|
+
creative-tree-row.chat-active .creative-row {
|
|
440
|
+
border: var(--border-1) solid var(--color-active);
|
|
441
|
+
border-radius: var(--radius-2);
|
|
442
|
+
box-shadow: 0 0 6px color-mix(in srgb, var(--color-active) 25%, transparent),
|
|
443
|
+
0 0 14px color-mix(in srgb, var(--color-active) 12%, transparent);
|
|
444
|
+
}
|
|
445
|
+
|
|
443
446
|
.creative-row:hover,
|
|
444
447
|
.creative-row.level-1:hover,
|
|
445
448
|
.creative-row.level-2:hover,
|
|
@@ -79,12 +79,7 @@ body.dark-mode {
|
|
|
79
79
|
/* ============================================================================
|
|
80
80
|
* BASE ELEMENT STYLES
|
|
81
81
|
* ============================================================================ */
|
|
82
|
-
html {
|
|
83
|
-
scroll-behavior: smooth;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
82
|
@media (prefers-reduced-motion: reduce) {
|
|
87
|
-
html { scroll-behavior: auto; }
|
|
88
83
|
|
|
89
84
|
/* Collapse decorative transitions to instant; state changes still
|
|
90
85
|
apply but without visible motion (hover bg, theme switch, etc.). */
|
|
@@ -302,3 +297,28 @@ input[type="datetime-local"]::-webkit-datetime-edit-minute-field {
|
|
|
302
297
|
.inline-block {
|
|
303
298
|
display: inline-block;
|
|
304
299
|
}
|
|
300
|
+
|
|
301
|
+
/* --- Dark mode: stronger glow for visibility on dark backgrounds --- */
|
|
302
|
+
body.dark-mode #comments-popup {
|
|
303
|
+
box-shadow: var(--shadow-3),
|
|
304
|
+
0 0 8px color-mix(in srgb, var(--color-active) 45%, transparent),
|
|
305
|
+
0 0 20px color-mix(in srgb, var(--color-active) 25%, transparent);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
body.dark-mode creative-tree-row.chat-active .creative-row {
|
|
309
|
+
box-shadow: 0 0 8px color-mix(in srgb, var(--color-active) 45%, transparent),
|
|
310
|
+
0 0 20px color-mix(in srgb, var(--color-active) 25%, transparent);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@media (prefers-color-scheme: dark) {
|
|
314
|
+
body:not(.light-mode):not(.dark-mode) #comments-popup {
|
|
315
|
+
box-shadow: var(--shadow-3),
|
|
316
|
+
0 0 8px color-mix(in srgb, var(--color-active) 45%, transparent),
|
|
317
|
+
0 0 20px color-mix(in srgb, var(--color-active) 25%, transparent);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
body:not(.light-mode):not(.dark-mode) creative-tree-row.chat-active .creative-row {
|
|
321
|
+
box-shadow: 0 0 8px color-mix(in srgb, var(--color-active) 45%, transparent),
|
|
322
|
+
0 0 20px color-mix(in srgb, var(--color-active) 25%, transparent);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -281,7 +281,8 @@ module Collavre
|
|
|
281
281
|
name: u.display_name,
|
|
282
282
|
avatar_url: view_context.user_avatar_url(u, size: 20),
|
|
283
283
|
default_avatar: !u.avatar.attached? && u.avatar_url.blank?,
|
|
284
|
-
initial: u.display_name[0].upcase
|
|
284
|
+
initial: u.display_name[0].upcase,
|
|
285
|
+
ai_user: u.ai_user?
|
|
285
286
|
}
|
|
286
287
|
end
|
|
287
288
|
render json: {
|
|
@@ -7,11 +7,12 @@ module Collavre
|
|
|
7
7
|
can_manage = @creative.has_permission?(Current.user, :admin) || is_owner
|
|
8
8
|
can_create_topic = can_manage || @creative.has_permission?(Current.user, :write)
|
|
9
9
|
|
|
10
|
-
active_topics = @creative.topics.active.order(:created_at)
|
|
10
|
+
active_topics = @creative.topics.active.order(:created_at).to_a
|
|
11
|
+
preload_primary_agents(active_topics)
|
|
11
12
|
archived_topics = @creative.topics.archived.order(:created_at)
|
|
12
13
|
|
|
13
14
|
render json: {
|
|
14
|
-
topics: active_topics,
|
|
15
|
+
topics: active_topics.map { |t| topic_json(t) },
|
|
15
16
|
archived_topics: archived_topics,
|
|
16
17
|
can_manage: can_manage,
|
|
17
18
|
can_create_topic: can_create_topic
|
|
@@ -27,9 +28,16 @@ module Collavre
|
|
|
27
28
|
topic.user = Current.user
|
|
28
29
|
|
|
29
30
|
if topic.save
|
|
31
|
+
agent = nil
|
|
32
|
+
if params[:agent_id].present?
|
|
33
|
+
agent = User.find_by(id: params[:agent_id])
|
|
34
|
+
topic.set_primary_agent!(agent) if agent&.ai_user?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
broadcast_data = agent ? topic_json_with_agent(topic, agent) : topic.slice(:id, :name)
|
|
30
38
|
TopicsChannel.broadcast_to(
|
|
31
39
|
@creative,
|
|
32
|
-
{ action: "created", topic:
|
|
40
|
+
{ action: "created", topic: broadcast_data, user_id: Current.user.id }
|
|
33
41
|
)
|
|
34
42
|
render json: topic, status: :created
|
|
35
43
|
else
|
|
@@ -159,6 +167,31 @@ module Collavre
|
|
|
159
167
|
render json: { success: true }
|
|
160
168
|
end
|
|
161
169
|
|
|
170
|
+
def set_primary_agent
|
|
171
|
+
unless @creative.has_permission?(Current.user, :write) || @creative.user == Current.user
|
|
172
|
+
render json: { error: I18n.t("collavre.topics.no_permission") }, status: :forbidden and return
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
topic = @creative.topics.find(params[:id])
|
|
176
|
+
agent = User.find_by(id: params[:agent_id])
|
|
177
|
+
|
|
178
|
+
unless agent&.ai_user?
|
|
179
|
+
render json: { error: I18n.t("collavre.topics.not_ai_agent") }, status: :unprocessable_entity and return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
topic.set_primary_agent!(agent)
|
|
183
|
+
|
|
184
|
+
TopicsChannel.broadcast_to(
|
|
185
|
+
@creative,
|
|
186
|
+
{
|
|
187
|
+
action: "updated",
|
|
188
|
+
topic: topic_json_with_agent(topic, agent)
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
render json: { success: true, topic: topic_json_with_agent(topic, agent) }
|
|
193
|
+
end
|
|
194
|
+
|
|
162
195
|
private
|
|
163
196
|
|
|
164
197
|
def set_creative
|
|
@@ -168,5 +201,49 @@ module Collavre
|
|
|
168
201
|
def topic_params
|
|
169
202
|
params.require(:topic).permit(:name)
|
|
170
203
|
end
|
|
204
|
+
|
|
205
|
+
# Batch-load primary agents for all topics to avoid N+1 queries
|
|
206
|
+
def preload_primary_agents(topics)
|
|
207
|
+
topic_ids = topics.map(&:id)
|
|
208
|
+
return if topic_ids.empty?
|
|
209
|
+
|
|
210
|
+
policies = OrchestratorPolicy.where(
|
|
211
|
+
policy_type: "arbitration",
|
|
212
|
+
scope_type: "Topic",
|
|
213
|
+
scope_id: topic_ids
|
|
214
|
+
).index_by { |p| p.scope_id.to_i }
|
|
215
|
+
|
|
216
|
+
agent_ids = policies.values.filter_map { |p| p.config&.dig("primary_agent_id") }
|
|
217
|
+
agents = agent_ids.present? ? User.where(id: agent_ids).includes(avatar_attachment: :blob).index_by(&:id) : {}
|
|
218
|
+
|
|
219
|
+
topics.each do |topic|
|
|
220
|
+
policy = policies[topic.id]
|
|
221
|
+
agent_id = policy&.config&.dig("primary_agent_id")
|
|
222
|
+
topic.instance_variable_set(:@_primary_agent, agents&.dig(agent_id))
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def topic_json(topic)
|
|
227
|
+
data = topic.slice(:id, :name)
|
|
228
|
+
agent = topic.instance_variable_get(:@_primary_agent) || topic.primary_agent
|
|
229
|
+
if agent
|
|
230
|
+
data[:primary_agent] = agent_json(agent)
|
|
231
|
+
end
|
|
232
|
+
data
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def topic_json_with_agent(topic, agent)
|
|
236
|
+
data = topic.slice(:id, :name)
|
|
237
|
+
data[:primary_agent] = agent_json(agent)
|
|
238
|
+
data
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def agent_json(agent)
|
|
242
|
+
{
|
|
243
|
+
id: agent.id,
|
|
244
|
+
name: agent.display_name,
|
|
245
|
+
avatar_url: view_context.user_avatar_url(agent, size: 20)
|
|
246
|
+
}
|
|
247
|
+
end
|
|
171
248
|
end
|
|
172
249
|
end
|
|
@@ -53,6 +53,11 @@ module Collavre
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# Creative admins can also edit actions even if not the designated approver
|
|
57
|
+
if status_in_lock != :ok && @creative.has_permission?(Current.user, :admin)
|
|
58
|
+
status_in_lock = :ok
|
|
59
|
+
end
|
|
60
|
+
|
|
56
61
|
if status_in_lock != :ok
|
|
57
62
|
approver_mismatch_error = true
|
|
58
63
|
status_error_key = case status_in_lock
|
|
@@ -37,6 +37,38 @@ module Collavre
|
|
|
37
37
|
head :no_content
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
def merge
|
|
41
|
+
comment_ids = Array(params[:comment_ids]).map(&:to_i).uniq
|
|
42
|
+
if comment_ids.size < 2
|
|
43
|
+
render json: { error: I18n.t("collavre.comments.merge.minimum_required") }, status: :unprocessable_entity and return
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
unless @creative.has_permission?(Current.user, :feedback)
|
|
47
|
+
render json: { error: I18n.t("collavre.comments.merge.not_authorized") }, status: :forbidden and return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
visible_scope = @creative.comments.where(
|
|
51
|
+
"comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
|
|
52
|
+
false, Current.user.id, Current.user.id
|
|
53
|
+
)
|
|
54
|
+
comments = visible_scope.where(id: comment_ids).to_a
|
|
55
|
+
|
|
56
|
+
if comments.length != comment_ids.length
|
|
57
|
+
render json: { error: I18n.t("collavre.comments.batch_delete_not_found") }, status: :not_found and return
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# All comments must belong to the current user or their AI agents
|
|
61
|
+
my_ai_agent_ids = Current.user.created_ai_users.pluck(:id)
|
|
62
|
+
allowed_user_ids = [ Current.user.id ] + my_ai_agent_ids
|
|
63
|
+
unauthorized = comments.reject { |c| allowed_user_ids.include?(c.user_id) }
|
|
64
|
+
if unauthorized.any?
|
|
65
|
+
render json: { error: I18n.t("collavre.comments.merge.own_messages_only") }, status: :forbidden and return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
MergeCommentsJob.perform_later(@creative.id, comment_ids, Current.user.id)
|
|
69
|
+
render json: { message: I18n.t("collavre.comments.merge.started") }, status: :accepted
|
|
70
|
+
end
|
|
71
|
+
|
|
40
72
|
def move
|
|
41
73
|
result = CommentMoveService.new(creative: @creative, user: Current.user).call(
|
|
42
74
|
comment_ids: params[:comment_ids],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { renderCommentMarkdown } from "../lib/utils/markdown"
|
|
2
3
|
|
|
3
4
|
export default class extends Controller {
|
|
4
5
|
static targets = ["prevBtn", "nextBtn", "indicator", "deleteBtn", "selectBtn"]
|
|
@@ -123,7 +124,10 @@ export default class extends Controller {
|
|
|
123
124
|
setContentText(text) {
|
|
124
125
|
const el = document.getElementById(this.contentTargetValue)
|
|
125
126
|
const target = el?.querySelector(".comment-content") || el?.querySelector("[data-comment-target='content']")
|
|
126
|
-
if (target)
|
|
127
|
+
if (target) {
|
|
128
|
+
target.innerHTML = renderCommentMarkdown(text)
|
|
129
|
+
target.dataset.rendered = "true"
|
|
130
|
+
}
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
updateButtons() {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
|
+
const CREATIVE_MIME_TYPE = 'application/x-collavre-creative'
|
|
4
|
+
|
|
3
5
|
export default class extends Controller {
|
|
4
6
|
static targets = ["list", "toggleButton"]
|
|
5
7
|
|
|
@@ -11,6 +13,7 @@ export default class extends Controller {
|
|
|
11
13
|
this.canManage = false
|
|
12
14
|
this.draggingContextId = null
|
|
13
15
|
this.listVisible = false
|
|
16
|
+
// External drop zone handlers are now Stimulus actions on the list target
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
get creativeId() {
|
|
@@ -22,6 +25,7 @@ export default class extends Controller {
|
|
|
22
25
|
this.listVisible = false
|
|
23
26
|
this._updateListVisibility()
|
|
24
27
|
await this.loadContexts()
|
|
28
|
+
this._bindPopupDragDetection()
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
onPopupClosed() {
|
|
@@ -30,6 +34,7 @@ export default class extends Controller {
|
|
|
30
34
|
if (this.hasListTarget) {
|
|
31
35
|
this.listTarget.innerHTML = ''
|
|
32
36
|
}
|
|
37
|
+
this._unbindPopupDragDetection()
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
async loadContexts() {
|
|
@@ -97,6 +102,7 @@ export default class extends Controller {
|
|
|
97
102
|
if (!this.hasListTarget) return
|
|
98
103
|
|
|
99
104
|
this._updateToggleButton()
|
|
105
|
+
// Drop zone is handled by Stimulus data-action on the list element
|
|
100
106
|
|
|
101
107
|
const dragActions = this.canManage
|
|
102
108
|
? 'dragstart->comments--contexts#handleDragStart dragend->comments--contexts#handleDragEnd'
|
|
@@ -295,11 +301,15 @@ export default class extends Controller {
|
|
|
295
301
|
}
|
|
296
302
|
|
|
297
303
|
async handleReorderDrop(event) {
|
|
298
|
-
event.preventDefault()
|
|
299
|
-
|
|
300
304
|
const targetEl = event.currentTarget
|
|
301
305
|
targetEl.classList.remove('context-drag-over-left', 'context-drag-over-right')
|
|
302
306
|
|
|
307
|
+
// If this is a creative drag (not a context reorder), let it bubble to the list handler
|
|
308
|
+
if (!event.dataTransfer.types.includes('application/x-context-id')) return
|
|
309
|
+
|
|
310
|
+
event.preventDefault()
|
|
311
|
+
event.stopPropagation()
|
|
312
|
+
|
|
303
313
|
const draggedId = parseInt(event.dataTransfer.getData('application/x-context-id'))
|
|
304
314
|
const targetId = parseInt(targetEl.dataset.contextId)
|
|
305
315
|
|
|
@@ -345,7 +355,7 @@ export default class extends Controller {
|
|
|
345
355
|
method: 'PATCH',
|
|
346
356
|
headers: {
|
|
347
357
|
'Content-Type': 'application/json',
|
|
348
|
-
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')
|
|
358
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
|
349
359
|
},
|
|
350
360
|
body: JSON.stringify(params)
|
|
351
361
|
})
|
|
@@ -358,6 +368,111 @@ export default class extends Controller {
|
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
370
|
|
|
371
|
+
// --- Auto-show context list when dragging creative over popup ---
|
|
372
|
+
_bindPopupDragDetection() {
|
|
373
|
+
const popup = this.element.closest('#comments-popup')
|
|
374
|
+
if (!popup) return
|
|
375
|
+
// Unbind any existing handlers first to prevent accumulation across opens
|
|
376
|
+
this._unbindPopupDragDetection()
|
|
377
|
+
this._popupEl = popup
|
|
378
|
+
this._boundPopupDragOver = this._handlePopupDragOver.bind(this)
|
|
379
|
+
this._boundPopupDragLeave = this._handlePopupDragLeave.bind(this)
|
|
380
|
+
this._boundPopupDrop = this.handleExternalDrop.bind(this)
|
|
381
|
+
popup.addEventListener('dragover', this._boundPopupDragOver)
|
|
382
|
+
popup.addEventListener('dragleave', this._boundPopupDragLeave)
|
|
383
|
+
popup.addEventListener('drop', this._boundPopupDrop)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_unbindPopupDragDetection() {
|
|
387
|
+
if (!this._popupEl) return
|
|
388
|
+
this._popupEl.removeEventListener('dragover', this._boundPopupDragOver)
|
|
389
|
+
this._popupEl.removeEventListener('dragleave', this._boundPopupDragLeave)
|
|
390
|
+
this._popupEl.removeEventListener('drop', this._boundPopupDrop)
|
|
391
|
+
this._popupEl = null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
_handlePopupDragOver(event) {
|
|
395
|
+
if (this._isInternalReorder(event)) return
|
|
396
|
+
if (!this._isCreativeDrag(event)) return
|
|
397
|
+
if (!this.canManage) return
|
|
398
|
+
|
|
399
|
+
// Must preventDefault to allow drop on the popup
|
|
400
|
+
event.preventDefault()
|
|
401
|
+
event.dataTransfer.dropEffect = 'move'
|
|
402
|
+
|
|
403
|
+
if (!this.listVisible) {
|
|
404
|
+
// Auto-show context list when dragging a creative over the popup
|
|
405
|
+
this.listVisible = true
|
|
406
|
+
this._updateListVisibility()
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_handlePopupDragLeave(event) {
|
|
411
|
+
if (!this._popupEl) return
|
|
412
|
+
// Only hide if leaving the popup entirely
|
|
413
|
+
if (this._popupEl.contains(event.relatedTarget)) return
|
|
414
|
+
if (this._hasBeenManuallyToggled) return
|
|
415
|
+
|
|
416
|
+
// Restore original state if no contexts
|
|
417
|
+
if (this.contexts.length === 0) {
|
|
418
|
+
this.listVisible = false
|
|
419
|
+
this._updateListVisibility()
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// --- Drop zone for adding creatives from tree ---
|
|
424
|
+
|
|
425
|
+
_isCreativeDrag(event) {
|
|
426
|
+
return event.dataTransfer.types.includes(CREATIVE_MIME_TYPE)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_isInternalReorder(event) {
|
|
430
|
+
return event.dataTransfer.types.includes('application/x-context-id')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
handleExternalDragOver(event) {
|
|
434
|
+
if (this._isInternalReorder(event)) return
|
|
435
|
+
if (!this._isCreativeDrag(event)) return
|
|
436
|
+
if (!this.canManage) return
|
|
437
|
+
|
|
438
|
+
event.preventDefault()
|
|
439
|
+
event.dataTransfer.dropEffect = 'move'
|
|
440
|
+
this.listTarget.classList.add('context-drop-active')
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
handleExternalDragLeave(event) {
|
|
444
|
+
// Only remove highlight if truly leaving the list area
|
|
445
|
+
if (!this.listTarget.contains(event.relatedTarget)) {
|
|
446
|
+
this.listTarget.classList.remove('context-drop-active')
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async handleExternalDrop(event) {
|
|
451
|
+
if (this._isInternalReorder(event)) return
|
|
452
|
+
if (!this._isCreativeDrag(event)) return
|
|
453
|
+
if (!this.canManage) return
|
|
454
|
+
|
|
455
|
+
// Always prevent default for creative drags to avoid browser navigation
|
|
456
|
+
event.preventDefault()
|
|
457
|
+
event.stopPropagation()
|
|
458
|
+
|
|
459
|
+
this.listTarget.classList.remove('context-drop-active')
|
|
460
|
+
|
|
461
|
+
let creativeId = null
|
|
462
|
+
|
|
463
|
+
const rawData = event.dataTransfer.getData(CREATIVE_MIME_TYPE)
|
|
464
|
+
if (rawData) {
|
|
465
|
+
try {
|
|
466
|
+
const parsed = JSON.parse(rawData)
|
|
467
|
+
creativeId = parseInt(parsed.creativeId)
|
|
468
|
+
} catch (e) { /* ignore */ }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!creativeId) return
|
|
472
|
+
|
|
473
|
+
await this._addContextId(creativeId)
|
|
474
|
+
}
|
|
475
|
+
|
|
361
476
|
_escapeHtml(text) {
|
|
362
477
|
const div = document.createElement('div')
|
|
363
478
|
div.textContent = text
|