collavre 0.5.0 → 0.7.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/comment_versions.css +76 -0
- data/app/assets/stylesheets/collavre/comments_popup.css +347 -37
- data/app/assets/stylesheets/collavre/creatives.css +73 -1
- data/app/assets/stylesheets/collavre/org_chart.css +319 -0
- data/app/assets/stylesheets/collavre/popup.css +68 -1
- data/app/controllers/collavre/application_controller.rb +13 -0
- data/app/controllers/collavre/comments/versions_controller.rb +82 -0
- data/app/controllers/collavre/comments_controller.rb +14 -153
- data/app/controllers/collavre/concerns/exportable.rb +30 -0
- data/app/controllers/collavre/concerns/shareable.rb +28 -0
- data/app/controllers/collavre/concerns/slide_viewable.rb +37 -0
- data/app/controllers/collavre/concerns/tree_manageable.rb +141 -0
- data/app/controllers/collavre/creative_imports_controller.rb +6 -0
- data/app/controllers/collavre/creative_invitations_controller.rb +46 -0
- data/app/controllers/collavre/creative_plans_controller.rb +1 -1
- data/app/controllers/collavre/creative_shares_controller.rb +84 -14
- data/app/controllers/collavre/creatives_controller.rb +70 -194
- data/app/controllers/collavre/google_auth_controller.rb +3 -0
- data/app/controllers/collavre/invites_controller.rb +2 -1
- data/app/controllers/collavre/sessions_controller.rb +3 -0
- data/app/controllers/collavre/topics_controller.rb +39 -2
- data/app/controllers/collavre/users_controller.rb +5 -404
- data/app/controllers/concerns/collavre/comments/approval_actions.rb +108 -0
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +55 -0
- data/app/controllers/concerns/collavre/comments/conversion.rb +46 -0
- data/app/controllers/concerns/collavre/users_controller/admin_operations.rb +74 -0
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +119 -0
- data/app/controllers/concerns/collavre/users_controller/contact_management.rb +166 -0
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +102 -0
- data/app/controllers/concerns/collavre/users_controller/registration.rb +63 -0
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/helpers/collavre/creatives_helper.rb +12 -9
- data/app/helpers/collavre/navigation_helper.rb +1 -1
- data/app/javascript/collavre.js +0 -1
- data/app/javascript/controllers/comment_controller.js +33 -70
- data/app/javascript/controllers/comment_version_controller.js +164 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +305 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_selection.test.js +103 -0
- data/app/javascript/controllers/comments/__tests__/review_quotes_store.test.js +113 -0
- data/app/javascript/controllers/comments/contexts_controller.js +363 -0
- data/app/javascript/controllers/comments/form_controller.js +304 -13
- data/app/javascript/controllers/comments/list_controller.js +151 -62
- data/app/javascript/controllers/comments/popup_controller.js +66 -38
- data/app/javascript/controllers/comments/presence_controller.js +2 -10
- data/app/javascript/controllers/comments/review_quotes_store.js +189 -0
- data/app/javascript/controllers/comments/topics_controller.js +34 -10
- data/app/javascript/controllers/index.js +15 -1
- data/app/javascript/controllers/org_chart_controller.js +46 -0
- data/app/javascript/controllers/share_modal_controller.js +369 -0
- data/app/javascript/controllers/topic_search_controller.js +103 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +42 -1
- data/app/javascript/lib/api/creatives.js +12 -0
- data/app/javascript/lib/api/csrf_fetch.js +35 -0
- data/app/javascript/lib/api/drag_drop.js +17 -0
- data/app/javascript/modules/command_menu.js +40 -0
- data/app/javascript/modules/creative_row_editor.js +88 -0
- data/app/javascript/modules/slide_view.js +2 -1
- data/app/jobs/collavre/ai_agent_job.rb +42 -30
- data/app/jobs/collavre/compress_job.rb +92 -0
- data/app/models/collavre/comment.rb +36 -1
- data/app/models/collavre/comment_version.rb +15 -0
- data/app/models/collavre/creative/describable.rb +1 -1
- data/app/models/collavre/creative.rb +51 -0
- data/app/models/collavre/task.rb +30 -2
- data/app/models/collavre/user.rb +20 -3
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +68 -0
- data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +89 -0
- data/app/services/collavre/ai_agent/message_builder.rb +85 -6
- data/app/services/collavre/ai_agent/response_finalizer.rb +97 -0
- data/app/services/collavre/ai_agent/response_streamer.rb +56 -0
- data/app/services/collavre/ai_agent/review_handler.rb +18 -1
- data/app/services/collavre/ai_agent_service.rb +130 -183
- data/app/services/collavre/ai_client.rb +6 -0
- data/app/services/collavre/auto_theme_generator.rb +1 -1
- data/app/services/collavre/command_menu_service.rb +19 -0
- data/app/services/collavre/comments/command_processor.rb +3 -1
- data/app/services/collavre/comments/compress_command.rb +75 -0
- data/app/services/collavre/comments/concerns/workflow_support.rb +115 -0
- data/app/services/collavre/comments/work_command.rb +161 -0
- data/app/services/collavre/comments/workflow_executor.rb +276 -0
- data/app/services/collavre/creatives/plan_tagger.rb +14 -3
- data/app/services/collavre/creatives/tree_formatter.rb +53 -13
- data/app/services/collavre/gemini_parent_recommender.rb +4 -4
- data/app/services/collavre/orchestration/agent_context_builder.rb +1 -3
- data/app/services/collavre/orchestration/agent_orchestrator.rb +15 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +0 -19
- data/app/services/collavre/orchestration/scheduler.rb +3 -2
- data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
- data/app/services/collavre/system_events/dispatcher.rb +9 -0
- data/app/services/collavre/tools/creative_create_service.rb +1 -8
- data/app/services/collavre/tools/creative_import_service.rb +46 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +157 -96
- data/app/services/collavre/tools/creative_update_service.rb +1 -8
- data/app/services/collavre/tools/cron_list_service.rb +1 -1
- data/app/services/collavre/tools/description_normalizable.rb +16 -0
- data/app/views/collavre/comments/_comment.html.erb +25 -8
- data/app/views/collavre/comments/_comments_popup.html.erb +32 -5
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +13 -0
- data/app/views/collavre/creatives/_share_button.html.erb +4 -1
- data/app/views/collavre/creatives/_share_modal.html.erb +31 -1
- data/app/views/collavre/creatives/index.html.erb +5 -5
- data/app/views/collavre/creatives/slide_view.html.erb +1 -1
- data/app/views/collavre/users/{_contact_management.html.erb → _contact_list.html.erb} +4 -8
- data/app/views/collavre/users/_org_chart.html.erb +68 -0
- data/app/views/collavre/users/_org_chart_node.html.erb +169 -0
- data/app/views/collavre/users/new_ai.html.erb +9 -0
- data/app/views/collavre/users/show.html.erb +32 -8
- data/config/locales/comments.en.yml +57 -2
- data/config/locales/comments.ko.yml +57 -2
- data/config/locales/contacts.en.yml +31 -0
- data/config/locales/contacts.ko.yml +31 -0
- data/config/locales/contexts.en.yml +8 -0
- data/config/locales/contexts.ko.yml +8 -0
- data/config/locales/creatives.en.yml +6 -0
- data/config/locales/creatives.ko.yml +6 -0
- data/config/locales/users.en.yml +1 -0
- data/config/locales/users.ko.yml +1 -0
- data/config/routes.rb +14 -1
- data/db/migrate/20260220072200_add_workflow_fields_to_tasks.rb +12 -0
- data/db/migrate/20260223173533_add_review_type_to_comments.rb +5 -0
- data/db/migrate/20260225065200_create_comment_versions.rb +14 -0
- data/db/migrate/20260225074416_add_selected_version_id_to_comments.rb +7 -0
- data/lib/collavre/version.rb +1 -1
- metadata +47 -10
- data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +0 -91
- data/app/javascript/lib/lexical/action_text_attachment_node.js +0 -459
- data/app/javascript/lib/lexical/dom_attachment_utils.js +0 -66
- data/app/javascript/modules/share_modal.js +0 -76
- data/app/javascript/modules/share_user_popup.js +0 -77
- data/app/services/collavre/orchestration/self_reflection_evaluator.rb +0 -231
- data/app/views/collavre/comments/_presence_avatars.html.erb +0 -8
- data/app/views/collavre/creatives/_delete_button.html.erb +0 -12
|
@@ -3,6 +3,8 @@ import { copyTextToClipboard } from '../../utils/clipboard'
|
|
|
3
3
|
import { renderMarkdownInContainer } from '../../lib/utils/markdown'
|
|
4
4
|
import creativesApi from '../../lib/api/creatives'
|
|
5
5
|
import { renderCreativeTree, dispatchCreativeTreeUpdated } from '../../creatives/tree_renderer'
|
|
6
|
+
import { updateCsrfTokenFromResponse } from '../../lib/api/csrf_fetch'
|
|
7
|
+
// CommonPopup is now used via TopicSearchController (Stimulus)
|
|
6
8
|
|
|
7
9
|
export default class extends Controller {
|
|
8
10
|
static targets = ['list']
|
|
@@ -138,6 +140,7 @@ export default class extends Controller {
|
|
|
138
140
|
|
|
139
141
|
loadInitialComments() {
|
|
140
142
|
if (!this.creativeId) return
|
|
143
|
+
if (this.selection.size > 0) return
|
|
141
144
|
|
|
142
145
|
const params = {}
|
|
143
146
|
if (this.highlightAfterLoad) {
|
|
@@ -170,6 +173,7 @@ export default class extends Controller {
|
|
|
170
173
|
this.initialLoadComplete = true
|
|
171
174
|
this.formController?.focusTextarea()
|
|
172
175
|
this.markCommentsRead()
|
|
176
|
+
|
|
173
177
|
})
|
|
174
178
|
}
|
|
175
179
|
|
|
@@ -239,6 +243,10 @@ export default class extends Controller {
|
|
|
239
243
|
urlParams.set('topic_id', this.currentTopicId)
|
|
240
244
|
}
|
|
241
245
|
return fetch(`/creatives/${this.creativeId}/comments?${urlParams.toString()}`).then((response) => {
|
|
246
|
+
// Keep the CSRF meta tag in sync with the session cookie.
|
|
247
|
+
// This is critical after the browser returns from a background/frozen state.
|
|
248
|
+
updateCsrfTokenFromResponse(response)
|
|
249
|
+
|
|
242
250
|
const serverTopicId = response.headers.get("X-Topic-Id")
|
|
243
251
|
if (serverTopicId !== null && serverTopicId !== undefined) {
|
|
244
252
|
// Server says we are in this topic.
|
|
@@ -382,11 +390,6 @@ export default class extends Controller {
|
|
|
382
390
|
this.closeActionEditor(this.getActionContainer(target.closest('.cancel-comment-action-edit-btn')))
|
|
383
391
|
return
|
|
384
392
|
}
|
|
385
|
-
if (target.classList.contains('delete-comment-btn')) {
|
|
386
|
-
event.preventDefault()
|
|
387
|
-
this.deleteComment(target)
|
|
388
|
-
return
|
|
389
|
-
}
|
|
390
393
|
if (target.classList.contains('convert-comment-btn')) {
|
|
391
394
|
event.preventDefault()
|
|
392
395
|
this.convertComment(target)
|
|
@@ -488,64 +491,129 @@ export default class extends Controller {
|
|
|
488
491
|
}
|
|
489
492
|
this.updateDraggableState()
|
|
490
493
|
this.notifySelectionChange()
|
|
491
|
-
this.
|
|
494
|
+
this.updateSelectionActionBar()
|
|
492
495
|
}
|
|
493
496
|
|
|
494
|
-
|
|
495
|
-
// Remove existing
|
|
496
|
-
const
|
|
497
|
-
if (
|
|
498
|
-
existingHint.remove()
|
|
499
|
-
}
|
|
497
|
+
updateSelectionActionBar() {
|
|
498
|
+
// Remove existing bar
|
|
499
|
+
const existing = document.querySelector('.selection-action-bar')
|
|
500
|
+
if (existing) existing.remove()
|
|
500
501
|
|
|
501
502
|
if (this.selection.size === 0) return
|
|
502
503
|
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
<div class="hint-
|
|
517
|
-
|
|
518
|
-
<span>${moveButtonText}</span>
|
|
504
|
+
const count = this.selection.size
|
|
505
|
+
const i18n = (key, fallback) => this.element.dataset[key] || fallback
|
|
506
|
+
|
|
507
|
+
const bar = document.createElement('div')
|
|
508
|
+
bar.className = 'selection-action-bar'
|
|
509
|
+
bar.innerHTML = `
|
|
510
|
+
<div class="selection-action-bar-main">
|
|
511
|
+
<span class="selection-action-bar-count">${i18n('selectionCountText', '{count}개 선택').replace('{count}', count)}</span>
|
|
512
|
+
<button type="button" class="selection-action-bar-btn selection-action-delete" title="${i18n('selectionDeleteText', 'Delete')}">🗑 ${i18n('selectionDeleteText', 'Delete')}</button>
|
|
513
|
+
<button type="button" class="selection-action-bar-btn selection-action-move" title="${i18n('selectionMoveText', 'Move')}">📤 ${i18n('selectionMoveText', 'Move')}</button>
|
|
514
|
+
<button type="button" class="selection-action-bar-btn selection-action-topic" title="${i18n('selectionTopicMoveText', 'Move to topic')}">🏷 ${i18n('selectionTopicMoveText', 'Move to topic')}</button>
|
|
515
|
+
<button type="button" class="selection-action-bar-close" title="${i18n('selectionCloseText', 'Cancel')}">✕</button>
|
|
516
|
+
</div>
|
|
517
|
+
<div class="selection-action-bar-hint no-touch">
|
|
518
|
+
💡 ${i18n('selectionDragHintText', 'Drag & drop to move to topic')}
|
|
519
519
|
</div>
|
|
520
520
|
`
|
|
521
521
|
|
|
522
|
-
|
|
523
|
-
|
|
522
|
+
bar.querySelector('.selection-action-delete').addEventListener('click', (e) => { e.stopPropagation(); this.deleteSelectedComments() })
|
|
523
|
+
bar.querySelector('.selection-action-move').addEventListener('click', (e) => this.openMoveModal(e))
|
|
524
|
+
bar.querySelector('.selection-action-topic').addEventListener('click', (e) => this.openTopicSearchPopup(e))
|
|
525
|
+
bar.querySelector('.selection-action-bar-close').addEventListener('click', () => this.clearSelection())
|
|
524
526
|
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
527
|
+
// Insert before typing indicator so it stays inside the popup window
|
|
528
|
+
const typingIndicator = this.element.querySelector('#typing-indicator')
|
|
529
|
+
if (typingIndicator) {
|
|
530
|
+
typingIndicator.parentNode.insertBefore(bar, typingIndicator)
|
|
531
|
+
} else {
|
|
532
|
+
this.element.appendChild(bar)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async deleteSelectedComments() {
|
|
537
|
+
if (this.selection.size === 0) return
|
|
538
|
+
const confirmText = this.element.dataset.batchDeleteConfirmText || 'Are you sure you want to delete the selected messages?'
|
|
539
|
+
if (!confirm(confirmText)) return
|
|
540
|
+
|
|
541
|
+
const commentIds = Array.from(this.selection)
|
|
542
|
+
try {
|
|
543
|
+
const response = await fetch(`/creatives/${this.creativeId}/comments/batch_destroy`, {
|
|
544
|
+
method: 'DELETE',
|
|
545
|
+
headers: {
|
|
546
|
+
'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content,
|
|
547
|
+
'Content-Type': 'application/json',
|
|
548
|
+
},
|
|
549
|
+
body: JSON.stringify({ comment_ids: commentIds }),
|
|
550
|
+
})
|
|
551
|
+
if (response.ok) {
|
|
552
|
+
commentIds.forEach((id) => {
|
|
553
|
+
const el = document.getElementById(`comment_${id}`)
|
|
554
|
+
if (el) el.remove()
|
|
555
|
+
})
|
|
556
|
+
this.clearSelection()
|
|
557
|
+
} else {
|
|
558
|
+
const data = await response.json().catch(() => ({}))
|
|
559
|
+
alert(data.error || 'Failed to delete comments')
|
|
560
|
+
}
|
|
561
|
+
} catch (error) {
|
|
562
|
+
console.error('Error deleting comments:', error)
|
|
563
|
+
alert('Failed to delete comments')
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
openTopicSearchPopup(event) {
|
|
568
|
+
if (this.selection.size === 0) return
|
|
530
569
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
570
|
+
const openWithController = (controller, btnRect) => {
|
|
571
|
+
controller.openForCreative(
|
|
572
|
+
this.creativeId,
|
|
573
|
+
btnRect,
|
|
574
|
+
(topic) => {
|
|
575
|
+
const commentIds = Array.from(this.selection)
|
|
576
|
+
this.handleMoveToTopic({ detail: { commentIds, targetTopicId: topic.id } })
|
|
577
|
+
},
|
|
578
|
+
this.element.dataset.topicMainText || 'Main'
|
|
579
|
+
)
|
|
580
|
+
}
|
|
534
581
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const maxTop = window.innerHeight - hintRect.height - boundsPadding
|
|
582
|
+
const btnRect = event.currentTarget.getBoundingClientRect()
|
|
583
|
+
let modal = document.getElementById('topic-search-modal')
|
|
538
584
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
585
|
+
if (modal) {
|
|
586
|
+
// Modal already exists — controller should be connected
|
|
587
|
+
const controller = this.application.getControllerForElementAndIdentifier(modal, 'topic-search')
|
|
588
|
+
if (controller) {
|
|
589
|
+
openWithController(controller, btnRect)
|
|
542
590
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// First time: create modal and wait for Stimulus to connect
|
|
595
|
+
modal = document.createElement('div')
|
|
596
|
+
modal.id = 'topic-search-modal'
|
|
597
|
+
modal.className = 'common-popup'
|
|
598
|
+
modal.style.display = 'none'
|
|
599
|
+
modal.dataset.controller = 'topic-search'
|
|
600
|
+
modal.innerHTML = `
|
|
601
|
+
<button type="button" class="popup-close-btn" data-topic-search-target="close">×</button>
|
|
602
|
+
<input type="text" class="shared-input-surface" style="width:100%;margin-bottom:0.5em;"
|
|
603
|
+
placeholder="${this.element.dataset.topicSearchPlaceholderText || 'Search topics...'}"
|
|
604
|
+
data-topic-search-target="input">
|
|
605
|
+
<ul class="common-popup-list" data-popup-list data-topic-search-target="list"></ul>
|
|
606
|
+
`
|
|
607
|
+
document.body.appendChild(modal)
|
|
546
608
|
|
|
547
|
-
|
|
548
|
-
|
|
609
|
+
// Wait for Stimulus to connect the controller, then open
|
|
610
|
+
requestAnimationFrame(() => {
|
|
611
|
+
const controller = this.application.getControllerForElementAndIdentifier(modal, 'topic-search')
|
|
612
|
+
if (controller) {
|
|
613
|
+
openWithController(controller, btnRect)
|
|
614
|
+
} else {
|
|
615
|
+
console.error('topic-search controller not found after creation')
|
|
616
|
+
}
|
|
549
617
|
})
|
|
550
618
|
}
|
|
551
619
|
|
|
@@ -635,9 +703,9 @@ export default class extends Controller {
|
|
|
635
703
|
if (item) item.classList.remove('selected-for-move')
|
|
636
704
|
})
|
|
637
705
|
this.notifySelectionChange()
|
|
638
|
-
// Remove
|
|
639
|
-
const
|
|
640
|
-
if (
|
|
706
|
+
// Remove action bar
|
|
707
|
+
const bar = document.querySelector('.selection-action-bar')
|
|
708
|
+
if (bar) bar.remove()
|
|
641
709
|
}
|
|
642
710
|
|
|
643
711
|
copyCommentLink(button) {
|
|
@@ -798,7 +866,7 @@ export default class extends Controller {
|
|
|
798
866
|
}
|
|
799
867
|
|
|
800
868
|
// Move Modal Logic
|
|
801
|
-
openMoveModal() {
|
|
869
|
+
openMoveModal(event) {
|
|
802
870
|
if (this.movingComments) return
|
|
803
871
|
if (this.selection.size === 0) {
|
|
804
872
|
alert(this.element.dataset.moveNoSelectionText || "No Selection")
|
|
@@ -806,16 +874,37 @@ export default class extends Controller {
|
|
|
806
874
|
}
|
|
807
875
|
this.movingComments = true
|
|
808
876
|
this.notifySelectionChange()
|
|
809
|
-
|
|
877
|
+
|
|
878
|
+
// Reuse the existing link-creative-modal and its controller
|
|
810
879
|
const modal = document.getElementById('link-creative-modal')
|
|
880
|
+
if (!modal) {
|
|
881
|
+
console.error('link-creative-modal not found')
|
|
882
|
+
this.movingComments = false
|
|
883
|
+
this.notifySelectionChange()
|
|
884
|
+
return
|
|
885
|
+
}
|
|
886
|
+
|
|
811
887
|
const controller = this.application.getControllerForElementAndIdentifier(modal, 'link-creative')
|
|
812
|
-
if (controller) {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
this.movingComments = false; this.notifySelectionChange()
|
|
888
|
+
if (!controller) {
|
|
889
|
+
console.error('link-creative controller not found')
|
|
890
|
+
this.movingComments = false
|
|
891
|
+
this.notifySelectionChange()
|
|
892
|
+
return
|
|
818
893
|
}
|
|
894
|
+
|
|
895
|
+
const btnRect = event.currentTarget.getBoundingClientRect()
|
|
896
|
+
controller.open(
|
|
897
|
+
btnRect,
|
|
898
|
+
(item) => {
|
|
899
|
+
// onSelect — move comments to selected creative
|
|
900
|
+
this.moveSelectedComments(item.id)
|
|
901
|
+
},
|
|
902
|
+
() => {
|
|
903
|
+
// onClose
|
|
904
|
+
this.movingComments = false
|
|
905
|
+
this.notifySelectionChange()
|
|
906
|
+
}
|
|
907
|
+
)
|
|
819
908
|
}
|
|
820
909
|
|
|
821
910
|
moveSelectedComments(targetId) {
|
|
@@ -827,10 +916,10 @@ export default class extends Controller {
|
|
|
827
916
|
body: JSON.stringify({ comment_ids: commentIds, target_creative_id: targetId })
|
|
828
917
|
}).then(r => r.ok ? r.json() : Promise.reject())
|
|
829
918
|
.then(() => {
|
|
830
|
-
this.
|
|
919
|
+
this.clearSelection()
|
|
831
920
|
this.loadInitialComments()
|
|
832
921
|
})
|
|
833
|
-
.finally(() => { this.movingComments = false
|
|
922
|
+
.finally(() => { this.movingComments = false })
|
|
834
923
|
}
|
|
835
924
|
|
|
836
925
|
// UI Helpers
|
|
@@ -34,8 +34,10 @@ export default class extends Controller {
|
|
|
34
34
|
this.handleWindowFocus = this.handleWindowFocus.bind(this)
|
|
35
35
|
this.handleVisibilityChange = this.handleVisibilityChange.bind(this)
|
|
36
36
|
this.handlePopState = this.handlePopState.bind(this)
|
|
37
|
+
this.handlePopupWheel = this.handlePopupWheel.bind(this)
|
|
37
38
|
|
|
38
39
|
document.addEventListener(CREATIVE_CLICK_EVENT, this.handleCreativeClick)
|
|
40
|
+
this.element.addEventListener('wheel', this.handlePopupWheel, { passive: false })
|
|
39
41
|
window.addEventListener('online', this.handleOnline)
|
|
40
42
|
window.addEventListener('focus', this.handleWindowFocus)
|
|
41
43
|
document.addEventListener('visibilitychange', this.handleVisibilityChange)
|
|
@@ -90,6 +92,7 @@ export default class extends Controller {
|
|
|
90
92
|
disconnect() {
|
|
91
93
|
this.clearPendingOpenFromUrl()
|
|
92
94
|
document.removeEventListener(CREATIVE_CLICK_EVENT, this.handleCreativeClick)
|
|
95
|
+
this.element.removeEventListener('wheel', this.handlePopupWheel)
|
|
93
96
|
window.removeEventListener('online', this.handleOnline)
|
|
94
97
|
window.removeEventListener('focus', this.handleWindowFocus)
|
|
95
98
|
document.removeEventListener('visibilitychange', this.handleVisibilityChange)
|
|
@@ -126,6 +129,10 @@ export default class extends Controller {
|
|
|
126
129
|
return this.application.getControllerForElementAndIdentifier(this.element, 'comments--topics')
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
get contextsController() {
|
|
133
|
+
return this.application.getControllerForElementAndIdentifier(this.element, 'comments--contexts')
|
|
134
|
+
}
|
|
135
|
+
|
|
129
136
|
handleCreativeClick(event) {
|
|
130
137
|
const button = event.detail?.button
|
|
131
138
|
const creativeId = event.detail?.creativeId
|
|
@@ -154,7 +161,6 @@ export default class extends Controller {
|
|
|
154
161
|
|
|
155
162
|
this.showPopup()
|
|
156
163
|
this.updatePosition()
|
|
157
|
-
document.body.classList.add('no-scroll')
|
|
158
164
|
|
|
159
165
|
await this.notifyChildControllers({ creativeId: resolvedCreativeId, canComment, highlightId })
|
|
160
166
|
|
|
@@ -211,6 +217,9 @@ export default class extends Controller {
|
|
|
211
217
|
if (this.mentionMenuController) {
|
|
212
218
|
this.mentionMenuController.onPopupOpened({ creativeId })
|
|
213
219
|
}
|
|
220
|
+
if (this.contextsController) {
|
|
221
|
+
this.contextsController.onPopupOpened({ creativeId })
|
|
222
|
+
}
|
|
214
223
|
}
|
|
215
224
|
|
|
216
225
|
close() {
|
|
@@ -229,6 +238,9 @@ export default class extends Controller {
|
|
|
229
238
|
if (this.topicsController) {
|
|
230
239
|
this.topicsController.onPopupClosed()
|
|
231
240
|
}
|
|
241
|
+
if (this.contextsController) {
|
|
242
|
+
this.contextsController.onPopupClosed()
|
|
243
|
+
}
|
|
232
244
|
|
|
233
245
|
// Dispatch event for integrations
|
|
234
246
|
this.element.dispatchEvent(new CustomEvent('comments-popup:closed', {
|
|
@@ -247,7 +259,6 @@ export default class extends Controller {
|
|
|
247
259
|
this.element.style.top = ''
|
|
248
260
|
this.element.style.bottom = ''
|
|
249
261
|
delete this.element.dataset.resized
|
|
250
|
-
document.body.classList.remove('no-scroll')
|
|
251
262
|
}
|
|
252
263
|
|
|
253
264
|
prepareSize() {
|
|
@@ -284,12 +295,10 @@ export default class extends Controller {
|
|
|
284
295
|
updatePosition() {
|
|
285
296
|
if (this.isFullscreen() || !this.currentButton || this.isMobile() || this.element.dataset.resized === 'true') return
|
|
286
297
|
const rect = this.currentButton.getBoundingClientRect()
|
|
287
|
-
|
|
288
|
-
let top = rect.bottom + scrollY + 4
|
|
298
|
+
let top = rect.bottom + 4
|
|
289
299
|
const bottom = top + this.element.offsetHeight
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
top = Math.max(scrollY + 4, viewportBottom - this.element.offsetHeight - 4)
|
|
300
|
+
if (bottom > window.innerHeight) {
|
|
301
|
+
top = Math.max(4, window.innerHeight - this.element.offsetHeight - 4)
|
|
293
302
|
}
|
|
294
303
|
this.element.style.top = `${top}px`
|
|
295
304
|
this.element.style.right = `${window.innerWidth - rect.right + 24}px`
|
|
@@ -303,8 +312,8 @@ export default class extends Controller {
|
|
|
303
312
|
this.resizeStartY = event.clientY
|
|
304
313
|
this.startWidth = rect.width
|
|
305
314
|
this.startHeight = rect.height
|
|
306
|
-
this.startLeft = rect.left
|
|
307
|
-
this.startTop = rect.top
|
|
315
|
+
this.startLeft = rect.left
|
|
316
|
+
this.startTop = rect.top
|
|
308
317
|
this.startBottom = this.startTop + this.startHeight
|
|
309
318
|
// this.reservedHeight = this.computeReservedHeight()
|
|
310
319
|
this.element.style.left = `${this.startLeft}px`
|
|
@@ -403,6 +412,32 @@ export default class extends Controller {
|
|
|
403
412
|
}
|
|
404
413
|
}
|
|
405
414
|
|
|
415
|
+
// Prevent wheel events on the popup from scrolling the background creative list
|
|
416
|
+
handlePopupWheel(event) {
|
|
417
|
+
if (this.isFullscreen()) return // fullscreen already blocks body scroll via CSS
|
|
418
|
+
|
|
419
|
+
if (!this.hasListTarget) {
|
|
420
|
+
event.preventDefault()
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const { scrollTop, scrollHeight, clientHeight } = this.listTarget
|
|
425
|
+
const isScrollingDown = event.deltaY > 0
|
|
426
|
+
const atTop = scrollTop <= 0
|
|
427
|
+
const atBottom = scrollTop + clientHeight >= scrollHeight - 1
|
|
428
|
+
|
|
429
|
+
// If the scrollable area has no overflow, or we're at the boundary, block propagation
|
|
430
|
+
if (scrollHeight <= clientHeight) {
|
|
431
|
+
event.preventDefault()
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// At boundaries, prevent the event from reaching the background
|
|
436
|
+
if ((isScrollingDown && atBottom) || (!isScrollingDown && atTop)) {
|
|
437
|
+
event.preventDefault()
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
406
441
|
// Enter fullscreen immediately without animation (for auto-fullscreen on page load)
|
|
407
442
|
_enterFullscreenImmediate() {
|
|
408
443
|
const el = this.element
|
|
@@ -535,17 +570,13 @@ export default class extends Controller {
|
|
|
535
570
|
}
|
|
536
571
|
|
|
537
572
|
// Desktop: animated exit to target position
|
|
538
|
-
// Calculate target position using
|
|
539
|
-
|
|
540
|
-
const scrollY = window.scrollY || window.pageYOffset
|
|
541
|
-
|
|
542
|
-
// Final absolute-position values (what updatePosition would set)
|
|
543
|
-
let finalTop = '' // px string with scrollY included
|
|
573
|
+
// Calculate target position using viewport-relative coords (popup is position: fixed)
|
|
574
|
+
let finalTop = '' // px string (viewport-relative)
|
|
544
575
|
let finalRight = '' // px string
|
|
545
576
|
let finalWidth = savedStyles?.width || ''
|
|
546
577
|
let finalHeight = savedStyles?.height || ''
|
|
547
578
|
|
|
548
|
-
//
|
|
579
|
+
// Animation targets (viewport-relative, same as final since popup is fixed)
|
|
549
580
|
let animTop, animLeft, animWidth, animHeight
|
|
550
581
|
|
|
551
582
|
// Try to find the comment button for precise positioning
|
|
@@ -555,6 +586,15 @@ export default class extends Controller {
|
|
|
555
586
|
targetButton = row?.querySelector('.comments-btn')
|
|
556
587
|
}
|
|
557
588
|
|
|
589
|
+
// Scroll the creative row into view instantly BEFORE calculating positions,
|
|
590
|
+
// so getBoundingClientRect returns viewport-visible coordinates
|
|
591
|
+
if (targetButton) {
|
|
592
|
+
const row = targetButton.closest('creative-tree-row')
|
|
593
|
+
if (row) {
|
|
594
|
+
row.scrollIntoView({ behavior: 'instant', block: 'center' })
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
558
598
|
if (targetButton) {
|
|
559
599
|
this.currentButton = targetButton
|
|
560
600
|
const btnRect = targetButton.getBoundingClientRect()
|
|
@@ -563,28 +603,25 @@ export default class extends Controller {
|
|
|
563
603
|
animWidth = parseFloat(finalWidth) || 420
|
|
564
604
|
animHeight = parseFloat(finalHeight) || 640
|
|
565
605
|
|
|
566
|
-
// Calculate top in
|
|
567
|
-
let
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
absTop = Math.max(scrollY + 4, viewportBottom - animHeight - 4)
|
|
606
|
+
// Calculate top in viewport coords — same as updatePosition
|
|
607
|
+
let top = btnRect.bottom + 4
|
|
608
|
+
const bottom = top + animHeight
|
|
609
|
+
if (bottom > window.innerHeight) {
|
|
610
|
+
top = Math.max(4, window.innerHeight - animHeight - 4)
|
|
572
611
|
}
|
|
573
612
|
|
|
574
|
-
|
|
575
|
-
finalTop = `${absTop}px`
|
|
613
|
+
finalTop = `${top}px`
|
|
576
614
|
finalRight = `${rightPx}px`
|
|
577
615
|
|
|
578
|
-
|
|
579
|
-
animTop = absTop - scrollY
|
|
616
|
+
animTop = top
|
|
580
617
|
animLeft = window.innerWidth - rightPx - animWidth
|
|
581
618
|
} else if (savedStyles && Object.values(savedStyles).some(v => v)) {
|
|
582
|
-
// Fallback to saved styles
|
|
619
|
+
// Fallback to saved styles (already viewport-relative since popup is fixed)
|
|
583
620
|
const rightVal = parseFloat(savedStyles.right) || 32
|
|
584
621
|
animWidth = parseFloat(savedStyles.width) || 420
|
|
585
622
|
animHeight = parseFloat(savedStyles.height) || 640
|
|
586
623
|
animLeft = savedStyles.left ? parseFloat(savedStyles.left) : (window.innerWidth - rightVal - animWidth)
|
|
587
|
-
animTop = parseFloat(savedStyles.top)
|
|
624
|
+
animTop = parseFloat(savedStyles.top) || 100
|
|
588
625
|
|
|
589
626
|
finalTop = savedStyles.top || ''
|
|
590
627
|
finalRight = savedStyles.right || ''
|
|
@@ -624,14 +661,12 @@ export default class extends Controller {
|
|
|
624
661
|
|
|
625
662
|
const cleanup = () => {
|
|
626
663
|
el.removeEventListener('transitionend', cleanup)
|
|
627
|
-
//
|
|
628
|
-
// Apply the pre-calculated absolute coords directly — no updatePosition() needed
|
|
664
|
+
// Popup is always position: fixed — just apply final coords
|
|
629
665
|
el.style.transition = 'none'
|
|
630
666
|
el.style.position = ''
|
|
631
667
|
el.style.bottom = ''
|
|
632
668
|
|
|
633
669
|
if (targetButton) {
|
|
634
|
-
// Set absolute coords matching updatePosition output
|
|
635
670
|
el.style.top = finalTop
|
|
636
671
|
el.style.right = finalRight
|
|
637
672
|
el.style.left = ''
|
|
@@ -671,13 +706,6 @@ export default class extends Controller {
|
|
|
671
706
|
}
|
|
672
707
|
this._previousUrl = null
|
|
673
708
|
|
|
674
|
-
// Scroll the selected creative row into view after exiting fullscreen
|
|
675
|
-
if (creativeId) {
|
|
676
|
-
requestAnimationFrame(() => {
|
|
677
|
-
const row = document.querySelector(`creative-tree-row[creative-id="${creativeId}"]`)
|
|
678
|
-
row?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
679
|
-
})
|
|
680
|
-
}
|
|
681
709
|
}
|
|
682
710
|
|
|
683
711
|
// Scroll to bottom after layout change
|
|
@@ -222,22 +222,14 @@ export default class extends Controller {
|
|
|
222
222
|
addBtn.className = 'add-participant-btn'
|
|
223
223
|
addBtn.textContent = '+'
|
|
224
224
|
addBtn.title = this.element.dataset.addParticipantText || 'Add user'
|
|
225
|
-
addBtn.
|
|
225
|
+
addBtn.dataset.action = 'click->share-modal#open'
|
|
226
|
+
addBtn.dataset.shareModalUrlParam = `/creatives/${this.creativeId}/creative_shares`
|
|
226
227
|
this.participantsTarget.appendChild(addBtn)
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
this.updateReadReceiptPresence(presentIds)
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
openShareModal() {
|
|
233
|
-
const modal = document.getElementById('share-creative-modal')
|
|
234
|
-
if (modal) {
|
|
235
|
-
modal.style.display = 'flex'
|
|
236
|
-
document.body.classList.add('no-scroll')
|
|
237
|
-
} else if (this.creativeId) {
|
|
238
|
-
window.location.href = `/creatives/${this.creativeId}?open_comments=true&open_share=true`
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
233
|
|
|
242
234
|
renderTypingIndicator() {
|
|
243
235
|
if (!this.hasTypingIndicatorTarget) return
|