collavre 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/collavre/comments_popup.css +293 -8
- data/app/assets/stylesheets/collavre/mention_menu.css +26 -0
- data/app/assets/stylesheets/collavre/popup.css +7 -0
- data/app/assets/stylesheets/collavre/print.css +18 -0
- data/app/channels/collavre/comments_presence_channel.rb +33 -0
- data/app/components/collavre/autocomplete_popup_component.html.erb +3 -0
- data/app/components/collavre/autocomplete_popup_component.rb +18 -0
- data/app/components/collavre/command_menu_component.rb +7 -0
- data/app/components/collavre/plans_timeline_component.html.erb +1 -1
- data/app/components/collavre/plans_timeline_component.rb +29 -32
- data/app/components/collavre/user_mention_menu_component.rb +4 -5
- data/app/controllers/collavre/comments_controller.rb +111 -10
- data/app/controllers/collavre/creatives_controller.rb +8 -0
- data/app/controllers/collavre/google_auth_controller.rb +5 -1
- data/app/controllers/collavre/plans_controller.rb +65 -9
- data/app/controllers/collavre/topics_controller.rb +42 -0
- data/app/controllers/collavre/users_controller.rb +4 -14
- data/app/errors/collavre/approval_pending_error.rb +54 -0
- data/app/errors/collavre/cancelled_error.rb +9 -0
- data/app/helpers/collavre/navigation_helper.rb +3 -1
- data/app/javascript/collavre.js +1 -0
- data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +2 -1
- data/app/javascript/controllers/comments/form_controller.js +2 -1
- data/app/javascript/controllers/comments/list_controller.js +185 -2
- data/app/javascript/controllers/comments/popup_controller.js +95 -20
- data/app/javascript/controllers/comments/presence_controller.js +30 -1
- data/app/javascript/controllers/comments/topics_controller.js +314 -4
- data/app/javascript/modules/__tests__/creative_progress.test.js +50 -0
- data/app/javascript/modules/command_menu.js +116 -0
- data/app/javascript/modules/creative_progress.js +14 -0
- data/app/javascript/modules/creative_row_editor.js +104 -20
- data/app/javascript/modules/plans_timeline.js +15 -4
- data/app/javascript/modules/share_modal.js +3 -0
- data/app/jobs/collavre/ai_agent_job.rb +35 -21
- data/app/models/collavre/calendar_event.rb +7 -1
- data/app/models/collavre/comment.rb +35 -2
- data/app/models/collavre/creative.rb +1 -3
- data/app/models/collavre/mcp_tool.rb +4 -0
- data/app/models/collavre/plan.rb +23 -0
- data/app/models/collavre/topic.rb +12 -0
- data/app/models/collavre/user.rb +15 -1
- data/app/services/collavre/ai_agent_service.rb +174 -66
- data/app/services/collavre/ai_client.rb +31 -2
- data/app/services/collavre/comments/action_executor.rb +47 -1
- data/app/services/collavre/comments/calendar_command.rb +117 -18
- data/app/services/collavre/google_calendar_service.rb +38 -15
- data/app/services/collavre/markdown_importer.rb +47 -8
- data/app/services/collavre/mcp_service.rb +23 -10
- data/app/services/collavre/system_events/router.rb +50 -26
- data/app/services/collavre/tools/creative_create_service.rb +97 -0
- data/app/services/collavre/tools/creative_update_service.rb +116 -0
- data/app/views/collavre/comments/_comment.html.erb +2 -2
- data/app/views/collavre/comments/_comments_popup.html.erb +40 -6
- data/app/views/collavre/comments/fullscreen.html.erb +5 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +11 -3
- data/app/views/collavre/creatives/_integration_modals.html.erb +6 -0
- data/app/views/collavre/creatives/_integration_triggers.html.erb +8 -0
- data/app/views/collavre/creatives/_integrations_menu.html.erb +12 -0
- data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +13 -1
- data/app/views/collavre/creatives/_share_button.html.erb +1 -1
- data/app/views/collavre/creatives/index.html.erb +22 -4
- data/app/views/collavre/users/edit_ai.html.erb +15 -0
- data/app/views/collavre/users/new_ai.html.erb +15 -0
- data/app/views/layouts/collavre/chat.html.erb +46 -0
- data/config/locales/ai_agent.en.yml +15 -0
- data/config/locales/ai_agent.ko.yml +15 -0
- data/config/locales/comments.en.yml +15 -3
- data/config/locales/comments.ko.yml +15 -3
- data/config/locales/creatives.en.yml +3 -31
- data/config/locales/creatives.ko.yml +3 -27
- data/config/locales/plans.en.yml +4 -0
- data/config/locales/plans.ko.yml +4 -0
- data/config/locales/users.en.yml +3 -0
- data/config/locales/users.ko.yml +3 -0
- data/config/routes.rb +8 -3
- data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +1 -1
- data/db/migrate/20260131100000_migrate_active_storage_attachment_record_types.rb +21 -0
- data/db/migrate/20260201100000_make_google_event_id_nullable.rb +5 -0
- data/lib/collavre/engine.rb +171 -6
- data/lib/collavre/integration_registry.rb +129 -0
- data/lib/collavre/version.rb +1 -1
- data/lib/collavre.rb +2 -0
- data/lib/navigation/registry.rb +130 -0
- metadata +22 -15
- data/app/components/collavre/user_mention_menu_component.html.erb +0 -3
- data/app/controllers/collavre/notion_auth_controller.rb +0 -25
- data/app/jobs/collavre/notion_export_job.rb +0 -30
- data/app/jobs/collavre/notion_sync_job.rb +0 -48
- data/app/models/collavre/notion_account.rb +0 -17
- data/app/models/collavre/notion_block_link.rb +0 -10
- data/app/models/collavre/notion_page_link.rb +0 -19
- data/app/services/collavre/notion_client.rb +0 -231
- data/app/services/collavre/notion_creative_exporter.rb +0 -296
- data/app/services/collavre/notion_service.rb +0 -249
- data/app/views/collavre/creatives/_notion_integration_modal.html.erb +0 -90
- data/db/migrate/20241201000000_create_notion_integrations.rb +0 -29
- data/db/migrate/20250312000000_create_notion_block_links.rb +0 -16
- data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +0 -5
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
|
2
2
|
import { copyTextToClipboard } from '../../utils/clipboard'
|
|
3
3
|
import { renderMarkdownInContainer } from '../../lib/utils/markdown'
|
|
4
|
+
import creativesApi from '../../lib/api/creatives'
|
|
5
|
+
import { renderCreativeTree, dispatchCreativeTreeUpdated } from '../../creatives/tree_renderer'
|
|
4
6
|
|
|
5
7
|
export default class extends Controller {
|
|
6
8
|
static targets = ['list']
|
|
@@ -44,6 +46,14 @@ export default class extends Controller {
|
|
|
44
46
|
|
|
45
47
|
this.handleTopicChange = this.handleTopicChange.bind(this)
|
|
46
48
|
this.element.addEventListener('comments--topics:change', this.handleTopicChange)
|
|
49
|
+
|
|
50
|
+
// Drag and drop handlers for moving comments to topics
|
|
51
|
+
this.handleDragStart = this.handleDragStart.bind(this)
|
|
52
|
+
this.handleDragEnd = this.handleDragEnd.bind(this)
|
|
53
|
+
this.handleMoveToTopic = this.handleMoveToTopic.bind(this)
|
|
54
|
+
this.listTarget.addEventListener('dragstart', this.handleDragStart)
|
|
55
|
+
this.listTarget.addEventListener('dragend', this.handleDragEnd)
|
|
56
|
+
this.element.addEventListener('comments--topics:move-to-topic', this.handleMoveToTopic)
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
handleTopicChange(event) {
|
|
@@ -56,12 +66,15 @@ export default class extends Controller {
|
|
|
56
66
|
this.listTarget.removeEventListener('change', this.handleChange)
|
|
57
67
|
this.listTarget.removeEventListener('click', this.handleClick)
|
|
58
68
|
this.listTarget.removeEventListener('submit', this.handleSubmit)
|
|
69
|
+
this.listTarget.removeEventListener('dragstart', this.handleDragStart)
|
|
70
|
+
this.listTarget.removeEventListener('dragend', this.handleDragEnd)
|
|
59
71
|
document.removeEventListener('turbo:before-stream-render', this.handleStreamRender)
|
|
60
72
|
if (this.listObserver) {
|
|
61
73
|
this.listObserver.disconnect()
|
|
62
74
|
this.listObserver = null
|
|
63
75
|
}
|
|
64
76
|
this.element.removeEventListener('comments--topics:change', this.handleTopicChange)
|
|
77
|
+
this.element.removeEventListener('comments--topics:move-to-topic', this.handleMoveToTopic)
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
isColumnReverse() {
|
|
@@ -459,12 +472,154 @@ export default class extends Controller {
|
|
|
459
472
|
const item = checkbox.closest('.comment-item')
|
|
460
473
|
if (checkbox.checked) {
|
|
461
474
|
this.selection.add(commentId)
|
|
462
|
-
if (item)
|
|
475
|
+
if (item) {
|
|
476
|
+
item.classList.add('selected-for-move')
|
|
477
|
+
item.setAttribute('draggable', 'true')
|
|
478
|
+
}
|
|
463
479
|
} else {
|
|
464
480
|
this.selection.delete(commentId)
|
|
465
|
-
if (item)
|
|
481
|
+
if (item) {
|
|
482
|
+
item.classList.remove('selected-for-move')
|
|
483
|
+
// Only remove draggable if no other items selected
|
|
484
|
+
if (this.selection.size === 0) {
|
|
485
|
+
item.removeAttribute('draggable')
|
|
486
|
+
}
|
|
487
|
+
}
|
|
466
488
|
}
|
|
489
|
+
this.updateDraggableState()
|
|
467
490
|
this.notifySelectionChange()
|
|
491
|
+
this.updateSelectionHint()
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
updateSelectionHint() {
|
|
495
|
+
// Remove existing hint
|
|
496
|
+
const existingHint = document.querySelector('.selection-hint-popup')
|
|
497
|
+
if (existingHint) {
|
|
498
|
+
existingHint.remove()
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (this.selection.size === 0) return
|
|
502
|
+
|
|
503
|
+
// Find the first selected checkbox
|
|
504
|
+
const firstSelected = this.listTarget.querySelector('.comment-item.selected-for-move')
|
|
505
|
+
if (!firstSelected) return
|
|
506
|
+
|
|
507
|
+
const checkbox = firstSelected.querySelector('.comment-select-checkbox')
|
|
508
|
+
if (!checkbox) return
|
|
509
|
+
|
|
510
|
+
// Create hint popup
|
|
511
|
+
const hint = document.createElement('div')
|
|
512
|
+
hint.className = 'selection-hint-popup'
|
|
513
|
+
const dragTopicText = this.element.dataset.hintDragTopicText || '🎯 Drag → Move to topic'
|
|
514
|
+
const moveButtonText = this.element.dataset.hintMoveButtonText || '📤 Move button → Another chat'
|
|
515
|
+
hint.innerHTML = `
|
|
516
|
+
<div class="hint-content">
|
|
517
|
+
<span>${dragTopicText}</span>
|
|
518
|
+
<span>${moveButtonText}</span>
|
|
519
|
+
</div>
|
|
520
|
+
`
|
|
521
|
+
|
|
522
|
+
// Append to body for proper positioning
|
|
523
|
+
document.body.appendChild(hint)
|
|
524
|
+
|
|
525
|
+
// Position like CommonPopup - to the right of checkbox, within viewport
|
|
526
|
+
requestAnimationFrame(() => {
|
|
527
|
+
const checkboxRect = checkbox.getBoundingClientRect()
|
|
528
|
+
const hintRect = hint.getBoundingClientRect()
|
|
529
|
+
const boundsPadding = 8
|
|
530
|
+
|
|
531
|
+
// Start to the right of the checkbox, vertically centered
|
|
532
|
+
let left = checkboxRect.right + 8
|
|
533
|
+
let top = checkboxRect.top + (checkboxRect.height / 2) - (hintRect.height / 2)
|
|
534
|
+
|
|
535
|
+
// Keep within viewport bounds
|
|
536
|
+
const maxLeft = window.innerWidth - hintRect.width - boundsPadding
|
|
537
|
+
const maxTop = window.innerHeight - hintRect.height - boundsPadding
|
|
538
|
+
|
|
539
|
+
// If overflows right, position to the left of checkbox instead
|
|
540
|
+
if (left > maxLeft) {
|
|
541
|
+
left = checkboxRect.left - hintRect.width - 8
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
left = Math.max(boundsPadding, Math.min(left, maxLeft))
|
|
545
|
+
top = Math.max(boundsPadding, Math.min(top, maxTop))
|
|
546
|
+
|
|
547
|
+
hint.style.left = `${left}px`
|
|
548
|
+
hint.style.top = `${top}px`
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
updateDraggableState() {
|
|
553
|
+
const hasSelection = this.selection.size > 0
|
|
554
|
+
this.listTarget.querySelectorAll('.comment-item').forEach((item) => {
|
|
555
|
+
const checkbox = item.querySelector('.comment-select-checkbox')
|
|
556
|
+
if (checkbox?.checked) {
|
|
557
|
+
item.setAttribute('draggable', 'true')
|
|
558
|
+
} else {
|
|
559
|
+
item.removeAttribute('draggable')
|
|
560
|
+
}
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
handleDragStart(event) {
|
|
565
|
+
const item = event.target.closest('.comment-item')
|
|
566
|
+
if (!item || this.selection.size === 0) {
|
|
567
|
+
event.preventDefault()
|
|
568
|
+
return
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Include all selected comment IDs
|
|
572
|
+
const commentIds = Array.from(this.selection)
|
|
573
|
+
event.dataTransfer.setData('application/x-comment-ids', JSON.stringify(commentIds))
|
|
574
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
575
|
+
|
|
576
|
+
// Add visual feedback
|
|
577
|
+
this.listTarget.classList.add('dragging-comments')
|
|
578
|
+
|
|
579
|
+
// Create custom drag image showing count
|
|
580
|
+
if (commentIds.length > 1) {
|
|
581
|
+
const dragImage = document.createElement('div')
|
|
582
|
+
dragImage.className = 'comment-drag-image'
|
|
583
|
+
dragImage.textContent = `${commentIds.length} messages`
|
|
584
|
+
document.body.appendChild(dragImage)
|
|
585
|
+
event.dataTransfer.setDragImage(dragImage, 0, 0)
|
|
586
|
+
setTimeout(() => dragImage.remove(), 0)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
handleDragEnd(event) {
|
|
591
|
+
this.listTarget.classList.remove('dragging-comments')
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async handleMoveToTopic(event) {
|
|
595
|
+
const { commentIds, targetTopicId } = event.detail
|
|
596
|
+
if (!commentIds || commentIds.length === 0 || !this.creativeId) return
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
const response = await fetch(`/creatives/${this.creativeId}/comments/move`, {
|
|
600
|
+
method: 'POST',
|
|
601
|
+
headers: {
|
|
602
|
+
'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content,
|
|
603
|
+
'Content-Type': 'application/json',
|
|
604
|
+
Accept: 'application/json'
|
|
605
|
+
},
|
|
606
|
+
body: JSON.stringify({
|
|
607
|
+
comment_ids: commentIds,
|
|
608
|
+
target_topic_id: targetTopicId
|
|
609
|
+
})
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
if (response.ok) {
|
|
613
|
+
this.clearSelection()
|
|
614
|
+
this.loadInitialComments()
|
|
615
|
+
} else {
|
|
616
|
+
const data = await response.json()
|
|
617
|
+
alert(data.error || 'Failed to move comments')
|
|
618
|
+
}
|
|
619
|
+
} catch (error) {
|
|
620
|
+
console.error('Error moving comments to topic:', error)
|
|
621
|
+
alert('Failed to move comments')
|
|
622
|
+
}
|
|
468
623
|
}
|
|
469
624
|
|
|
470
625
|
notifySelectionChange() {
|
|
@@ -480,6 +635,9 @@ export default class extends Controller {
|
|
|
480
635
|
if (item) item.classList.remove('selected-for-move')
|
|
481
636
|
})
|
|
482
637
|
this.notifySelectionChange()
|
|
638
|
+
// Remove hint popup from body
|
|
639
|
+
const hint = document.querySelector('.selection-hint-popup')
|
|
640
|
+
if (hint) hint.remove()
|
|
483
641
|
}
|
|
484
642
|
|
|
485
643
|
copyCommentLink(button) {
|
|
@@ -562,10 +720,35 @@ export default class extends Controller {
|
|
|
562
720
|
// Conversion usually converts to creative, so maybe reload or redirect?
|
|
563
721
|
// Original code reloaded initial comments. Safe to do:
|
|
564
722
|
this.loadInitialComments()
|
|
723
|
+
this.reloadCreativeChildren()
|
|
565
724
|
}
|
|
566
725
|
})
|
|
567
726
|
}
|
|
568
727
|
|
|
728
|
+
reloadCreativeChildren() {
|
|
729
|
+
if (!this.creativeId) return Promise.resolve()
|
|
730
|
+
const container = document.getElementById(`creative-children-${this.creativeId}`)
|
|
731
|
+
const loadUrl = container?.dataset?.loadUrl
|
|
732
|
+
if (!container || !loadUrl) {
|
|
733
|
+
this.reloadCreativeTree()
|
|
734
|
+
return Promise.resolve()
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return creativesApi.loadChildren(loadUrl).then((data) => {
|
|
738
|
+
const nodes = Array.isArray(data?.creatives) ? data.creatives : []
|
|
739
|
+
renderCreativeTree(container, nodes, { replace: false })
|
|
740
|
+
container.dataset.loaded = 'true'
|
|
741
|
+
dispatchCreativeTreeUpdated(container)
|
|
742
|
+
})
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
reloadCreativeTree() {
|
|
746
|
+
const treeElement = document.getElementById('creatives')
|
|
747
|
+
if (!treeElement) return
|
|
748
|
+
const controller = this.application.getControllerForElementAndIdentifier(treeElement, 'creatives--tree')
|
|
749
|
+
controller?.load?.()
|
|
750
|
+
}
|
|
751
|
+
|
|
569
752
|
approveComment(button) {
|
|
570
753
|
// ... (Existing logic) ...
|
|
571
754
|
if (button.disabled) return
|
|
@@ -11,6 +11,7 @@ export default class extends Controller {
|
|
|
11
11
|
'closeButton',
|
|
12
12
|
'leftHandle',
|
|
13
13
|
'rightHandle',
|
|
14
|
+
'fullscreenLink',
|
|
14
15
|
]
|
|
15
16
|
|
|
16
17
|
connect() {
|
|
@@ -36,23 +37,36 @@ export default class extends Controller {
|
|
|
36
37
|
window.addEventListener('focus', this.handleWindowFocus)
|
|
37
38
|
document.addEventListener('visibilitychange', this.handleVisibilityChange)
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
if (this.hasCloseButtonTarget) {
|
|
41
|
+
this.closeButtonTarget.addEventListener('click', () => this.close())
|
|
42
|
+
}
|
|
43
|
+
if (this.hasLeftHandleTarget) {
|
|
44
|
+
this.leftHandleTarget.addEventListener('mousedown', (event) => this.startResize(event, 'left'))
|
|
45
|
+
}
|
|
46
|
+
if (this.hasRightHandleTarget) {
|
|
47
|
+
this.rightHandleTarget.addEventListener('mousedown', (event) => this.startResize(event, 'right'))
|
|
48
|
+
}
|
|
42
49
|
|
|
43
50
|
if (this.isMobile()) {
|
|
44
51
|
// Handle touch events directly on the close button to resolve issues on mobile where layout shifts (e.g., keyboard dismissal) cause click events to be lost or delayed.
|
|
45
52
|
this.element.addEventListener('touchstart', this.handleTouchStart)
|
|
46
53
|
this.element.addEventListener('touchend', this.handleTouchEnd)
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
if (this.hasCloseButtonTarget) {
|
|
55
|
+
this.closeButtonTarget.addEventListener('touchstart', this.handleCloseButtonTouchStart, { passive: false })
|
|
56
|
+
this.closeButtonTarget.addEventListener('touchend', this.handleCloseButtonTouchEnd)
|
|
57
|
+
}
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
document.querySelectorAll('form[action="/session"]').forEach((form) => {
|
|
52
61
|
form.addEventListener('submit', () => window.localStorage.removeItem(SIZE_STORAGE_KEY))
|
|
53
62
|
})
|
|
54
63
|
|
|
55
|
-
this.
|
|
64
|
+
if (this.isFullscreen()) {
|
|
65
|
+
// Defer to ensure all sibling controllers are connected
|
|
66
|
+
requestAnimationFrame(() => this.openForCreative())
|
|
67
|
+
} else {
|
|
68
|
+
this.openFromUrl()
|
|
69
|
+
}
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
disconnect() {
|
|
@@ -66,8 +80,10 @@ export default class extends Controller {
|
|
|
66
80
|
if (this.isMobile()) {
|
|
67
81
|
this.element.removeEventListener('touchstart', this.handleTouchStart)
|
|
68
82
|
this.element.removeEventListener('touchend', this.handleTouchEnd)
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
if (this.hasCloseButtonTarget) {
|
|
84
|
+
this.closeButtonTarget.removeEventListener('touchstart', this.handleCloseButtonTouchStart)
|
|
85
|
+
this.closeButtonTarget.removeEventListener('touchend', this.handleCloseButtonTouchEnd)
|
|
86
|
+
}
|
|
71
87
|
}
|
|
72
88
|
}
|
|
73
89
|
|
|
@@ -115,29 +131,70 @@ export default class extends Controller {
|
|
|
115
131
|
this.element.dataset.canComment = canComment ? 'true' : 'false'
|
|
116
132
|
this.titleTarget.textContent = snippet
|
|
117
133
|
|
|
134
|
+
this.updateFullscreenLink(resolvedCreativeId)
|
|
135
|
+
|
|
118
136
|
this.prepareSize()
|
|
119
137
|
|
|
120
138
|
this.showPopup()
|
|
121
139
|
this.updatePosition()
|
|
122
140
|
document.body.classList.add('no-scroll')
|
|
123
141
|
|
|
142
|
+
await this.notifyChildControllers({ creativeId: resolvedCreativeId, canComment, highlightId })
|
|
143
|
+
|
|
144
|
+
// Dispatch event for integrations (e.g., Slack badge)
|
|
145
|
+
this.element.dispatchEvent(new CustomEvent('comments-popup:opened', {
|
|
146
|
+
bubbles: true,
|
|
147
|
+
detail: {
|
|
148
|
+
creativeId: resolvedCreativeId,
|
|
149
|
+
badgeContainer: this.element.querySelector('[data-integration-badges]')
|
|
150
|
+
}
|
|
151
|
+
}))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async openForCreative() {
|
|
155
|
+
const resolvedCreativeId = this.element.dataset.creativeId
|
|
156
|
+
const canComment = this.element.dataset.canComment === 'true'
|
|
157
|
+
const snippet = this.element.dataset.creativeSnippet || ''
|
|
158
|
+
|
|
159
|
+
this.currentButton = null
|
|
160
|
+
this.element.dataset.creativeId = resolvedCreativeId || ''
|
|
161
|
+
this.element.dataset.canComment = canComment ? 'true' : 'false'
|
|
162
|
+
this.titleTarget.textContent = snippet
|
|
163
|
+
|
|
164
|
+
this.updateFullscreenLink(resolvedCreativeId)
|
|
165
|
+
|
|
166
|
+
this.showPopup()
|
|
167
|
+
|
|
168
|
+
await this.notifyChildControllers({ creativeId: resolvedCreativeId, canComment })
|
|
169
|
+
|
|
170
|
+
// Dispatch event for integrations (e.g., Slack badge)
|
|
171
|
+
this.element.dispatchEvent(new CustomEvent('comments-popup:opened', {
|
|
172
|
+
bubbles: true,
|
|
173
|
+
detail: {
|
|
174
|
+
creativeId: resolvedCreativeId,
|
|
175
|
+
badgeContainer: this.element.querySelector('[data-integration-badges]')
|
|
176
|
+
}
|
|
177
|
+
}))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async notifyChildControllers({ creativeId, canComment, highlightId }) {
|
|
124
181
|
// Load topics first to establish context
|
|
125
182
|
if (this.topicsController) {
|
|
126
|
-
await this.topicsController.onPopupOpened({ creativeId
|
|
183
|
+
await this.topicsController.onPopupOpened({ creativeId })
|
|
127
184
|
}
|
|
128
185
|
|
|
129
186
|
if (this.formController) {
|
|
130
|
-
this.formController.onPopupOpened({ creativeId
|
|
187
|
+
this.formController.onPopupOpened({ creativeId, canComment })
|
|
131
188
|
}
|
|
132
189
|
if (this.listController) {
|
|
133
190
|
const topicId = this.topicsController ? this.topicsController.currentTopicId : undefined
|
|
134
|
-
this.listController.onPopupOpened({ creativeId
|
|
191
|
+
this.listController.onPopupOpened({ creativeId, highlightId, topicId })
|
|
135
192
|
}
|
|
136
193
|
if (this.presenceController) {
|
|
137
|
-
this.presenceController.onPopupOpened({ creativeId
|
|
194
|
+
this.presenceController.onPopupOpened({ creativeId })
|
|
138
195
|
}
|
|
139
196
|
if (this.mentionMenuController) {
|
|
140
|
-
this.mentionMenuController.onPopupOpened({ creativeId
|
|
197
|
+
this.mentionMenuController.onPopupOpened({ creativeId })
|
|
141
198
|
}
|
|
142
199
|
}
|
|
143
200
|
|
|
@@ -158,6 +215,14 @@ export default class extends Controller {
|
|
|
158
215
|
this.topicsController.onPopupClosed()
|
|
159
216
|
}
|
|
160
217
|
|
|
218
|
+
// Dispatch event for integrations
|
|
219
|
+
this.element.dispatchEvent(new CustomEvent('comments-popup:closed', {
|
|
220
|
+
bubbles: true,
|
|
221
|
+
detail: {
|
|
222
|
+
badgeContainer: this.element.querySelector('[data-integration-badges]')
|
|
223
|
+
}
|
|
224
|
+
}))
|
|
225
|
+
|
|
161
226
|
this.element.style.display = 'none'
|
|
162
227
|
this.element.classList.remove('open')
|
|
163
228
|
this.element.style.width = ''
|
|
@@ -187,20 +252,22 @@ export default class extends Controller {
|
|
|
187
252
|
|
|
188
253
|
|
|
189
254
|
showPopup() {
|
|
255
|
+
this.element.style.display = 'flex'
|
|
190
256
|
if (this.isMobile()) {
|
|
191
|
-
this.element.style.display = 'flex'
|
|
192
257
|
this.element.classList.add('open')
|
|
193
|
-
} else {
|
|
194
|
-
this.element.style.display = 'flex'
|
|
195
258
|
}
|
|
196
259
|
}
|
|
197
260
|
|
|
261
|
+
isFullscreen() {
|
|
262
|
+
return this.element.dataset.fullscreen === 'true'
|
|
263
|
+
}
|
|
264
|
+
|
|
198
265
|
isMobile() {
|
|
199
266
|
return window.innerWidth <= 600
|
|
200
267
|
}
|
|
201
268
|
|
|
202
269
|
updatePosition() {
|
|
203
|
-
if (!this.currentButton || this.isMobile() || this.element.dataset.resized === 'true') return
|
|
270
|
+
if (this.isFullscreen() || !this.currentButton || this.isMobile() || this.element.dataset.resized === 'true') return
|
|
204
271
|
const rect = this.currentButton.getBoundingClientRect()
|
|
205
272
|
const scrollY = window.scrollY || window.pageYOffset
|
|
206
273
|
let top = rect.bottom + scrollY + 4
|
|
@@ -321,8 +388,16 @@ export default class extends Controller {
|
|
|
321
388
|
}
|
|
322
389
|
}
|
|
323
390
|
|
|
391
|
+
updateFullscreenLink(creativeId) {
|
|
392
|
+
if (!this.hasFullscreenLinkTarget || !creativeId) return
|
|
393
|
+
const template = this.element.dataset.fullscreenUrlTemplate
|
|
394
|
+
if (!template) return
|
|
395
|
+
this.fullscreenLinkTarget.href = template.replace('__CREATIVE_ID__', creativeId)
|
|
396
|
+
}
|
|
397
|
+
|
|
324
398
|
openFromUrl() {
|
|
325
399
|
const params = new URLSearchParams(window.location.search)
|
|
400
|
+
const openComments = params.get('open_comments') === 'true'
|
|
326
401
|
let commentId = params.get('comment_id')
|
|
327
402
|
if (!commentId) {
|
|
328
403
|
const pathCommentMatch = window.location.pathname.match(/\/creatives\/\d+\/comments\/(\d+)/)
|
|
@@ -345,13 +420,14 @@ export default class extends Controller {
|
|
|
345
420
|
}
|
|
346
421
|
}
|
|
347
422
|
|
|
348
|
-
|
|
423
|
+
// Need either open_comments flag or comment_id, plus creativeId
|
|
424
|
+
if ((!commentId && !openComments) || !creativeId) return
|
|
349
425
|
const selector = `[name="show-comments-btn"][data-creative-id="${creativeId}"]`
|
|
350
426
|
const tryOpenWithButton = () => {
|
|
351
427
|
const button = document.querySelector(selector)
|
|
352
428
|
if (!button) return false
|
|
353
429
|
this.clearPendingOpenFromUrl()
|
|
354
|
-
this.open(button, { highlightId: commentId })
|
|
430
|
+
this.open(button, { highlightId: commentId || undefined })
|
|
355
431
|
return true
|
|
356
432
|
}
|
|
357
433
|
|
|
@@ -381,5 +457,4 @@ export default class extends Controller {
|
|
|
381
457
|
this.openFromUrlTimeout = null
|
|
382
458
|
}
|
|
383
459
|
}
|
|
384
|
-
|
|
385
460
|
}
|
|
@@ -91,7 +91,8 @@ export default class extends Controller {
|
|
|
91
91
|
fetch(`/creatives/${this.creativeId}/comments/participants`)
|
|
92
92
|
.then((response) => response.json())
|
|
93
93
|
.then((data) => {
|
|
94
|
-
this.participantsData = data
|
|
94
|
+
this.participantsData = data.users
|
|
95
|
+
this.canShare = data.can_share
|
|
95
96
|
this.renderParticipants(this.currentPresentIds)
|
|
96
97
|
this.renderTypingIndicator()
|
|
97
98
|
})
|
|
@@ -149,6 +150,15 @@ export default class extends Controller {
|
|
|
149
150
|
}
|
|
150
151
|
this.renderTypingIndicator()
|
|
151
152
|
}
|
|
153
|
+
if (data.agent_status) {
|
|
154
|
+
const { id, name, status } = data.agent_status
|
|
155
|
+
if (status === 'thinking' || status === 'streaming') {
|
|
156
|
+
this.typingUsers[id] = name
|
|
157
|
+
} else {
|
|
158
|
+
delete this.typingUsers[id]
|
|
159
|
+
}
|
|
160
|
+
this.renderTypingIndicator()
|
|
161
|
+
}
|
|
152
162
|
}
|
|
153
163
|
|
|
154
164
|
renderParticipants(presentIds) {
|
|
@@ -190,9 +200,28 @@ export default class extends Controller {
|
|
|
190
200
|
this.participantsTarget.appendChild(wrapper)
|
|
191
201
|
})
|
|
192
202
|
|
|
203
|
+
if (this.canShare) {
|
|
204
|
+
const addBtn = document.createElement('button')
|
|
205
|
+
addBtn.className = 'add-participant-btn'
|
|
206
|
+
addBtn.textContent = '+'
|
|
207
|
+
addBtn.title = this.element.dataset.addParticipantText || 'Add user'
|
|
208
|
+
addBtn.addEventListener('click', () => this.openShareModal())
|
|
209
|
+
this.participantsTarget.appendChild(addBtn)
|
|
210
|
+
}
|
|
211
|
+
|
|
193
212
|
this.updateReadReceiptPresence(presentIds)
|
|
194
213
|
}
|
|
195
214
|
|
|
215
|
+
openShareModal() {
|
|
216
|
+
const modal = document.getElementById('share-creative-modal')
|
|
217
|
+
if (modal) {
|
|
218
|
+
modal.style.display = 'flex'
|
|
219
|
+
document.body.classList.add('no-scroll')
|
|
220
|
+
} else if (this.creativeId) {
|
|
221
|
+
window.location.href = `/creatives/${this.creativeId}?open_comments=true&open_share=true`
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
196
225
|
renderTypingIndicator() {
|
|
197
226
|
if (!this.hasTypingIndicatorTarget) return
|
|
198
227
|
this.typingIndicatorTarget.innerHTML = ''
|