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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
|
2
2
|
import { renderMarkdownInContainer } from '../../lib/utils/markdown'
|
|
3
3
|
import { wrapHtmlInCodeBlocks } from '../../lib/html_code_block_wrapper'
|
|
4
|
+
import { refreshCsrfToken } from '../../lib/api/csrf_fetch'
|
|
5
|
+
import ReviewQuotesStore from './review_quotes_store'
|
|
4
6
|
|
|
5
7
|
export default class extends Controller {
|
|
6
8
|
static targets = [
|
|
@@ -9,7 +11,6 @@ export default class extends Controller {
|
|
|
9
11
|
'submit',
|
|
10
12
|
'privateCheckbox',
|
|
11
13
|
'cancel',
|
|
12
|
-
'moveButton',
|
|
13
14
|
'searchButton',
|
|
14
15
|
'voiceButton',
|
|
15
16
|
'imageInput',
|
|
@@ -19,12 +20,14 @@ export default class extends Controller {
|
|
|
19
20
|
'quotedText',
|
|
20
21
|
'quoteIndicator',
|
|
21
22
|
'quoteIndicatorText',
|
|
23
|
+
'reviewQuotesContainer',
|
|
22
24
|
]
|
|
23
25
|
|
|
24
26
|
connect() {
|
|
25
27
|
this.creativeId = null
|
|
26
28
|
this.editingId = null
|
|
27
29
|
this.sending = false
|
|
30
|
+
this._reviewStore = new ReviewQuotesStore()
|
|
28
31
|
this.cachedImageFiles = null
|
|
29
32
|
|
|
30
33
|
this.handleSubmit = this.handleSubmit.bind(this)
|
|
@@ -33,7 +36,6 @@ export default class extends Controller {
|
|
|
33
36
|
this.handlePointerSend = this.handlePointerSend.bind(this)
|
|
34
37
|
this.handleTouchSend = this.handleTouchSend.bind(this)
|
|
35
38
|
this.handleCancel = this.handleCancel.bind(this)
|
|
36
|
-
this.handleMoveClick = this.handleMoveClick.bind(this)
|
|
37
39
|
this.handleSearch = this.handleSearch.bind(this)
|
|
38
40
|
this.handleVoiceToggle = this.handleVoiceToggle.bind(this)
|
|
39
41
|
this.handleRecognitionStart = this.handleRecognitionStart.bind(this)
|
|
@@ -50,7 +52,6 @@ export default class extends Controller {
|
|
|
50
52
|
this.submitTarget.addEventListener('pointerup', this.handlePointerSend)
|
|
51
53
|
this.submitTarget.addEventListener('touchend', this.handleTouchSend, { passive: false })
|
|
52
54
|
this.cancelTarget?.addEventListener('click', this.handleCancel)
|
|
53
|
-
this.moveButtonTarget?.addEventListener('click', this.handleMoveClick)
|
|
54
55
|
this.searchButtonTarget?.addEventListener('click', this.handleSearch)
|
|
55
56
|
this.voiceButtonTarget?.addEventListener('click', this.handleVoiceToggle)
|
|
56
57
|
|
|
@@ -66,6 +67,15 @@ export default class extends Controller {
|
|
|
66
67
|
this.listening = false
|
|
67
68
|
this.recognitionActive = false
|
|
68
69
|
|
|
70
|
+
// Auto-resize textarea
|
|
71
|
+
this._autoResize = () => {
|
|
72
|
+
const textarea = this.textareaTarget
|
|
73
|
+
textarea.style.height = 'auto'
|
|
74
|
+
const maxHeight = parseInt(getComputedStyle(textarea).lineHeight, 10) * 10 || 200
|
|
75
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`
|
|
76
|
+
}
|
|
77
|
+
this.textareaTarget.addEventListener('input', this._autoResize)
|
|
78
|
+
|
|
69
79
|
this.textareaTarget.addEventListener('keydown', (event) => {
|
|
70
80
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
71
81
|
if (this.isMentionMenuVisible()) return
|
|
@@ -96,12 +106,12 @@ export default class extends Controller {
|
|
|
96
106
|
this.submitTarget.removeEventListener('pointerup', this.handlePointerSend)
|
|
97
107
|
this.submitTarget.removeEventListener('touchend', this.handleTouchSend)
|
|
98
108
|
this.cancelTarget?.removeEventListener('click', this.handleCancel)
|
|
99
|
-
this.moveButtonTarget?.removeEventListener('click', this.handleMoveClick)
|
|
100
109
|
this.searchButtonTarget?.removeEventListener('click', this.handleSearch)
|
|
101
110
|
this.voiceButtonTarget?.removeEventListener('click', this.handleVoiceToggle)
|
|
102
111
|
this.teardownSpeechRecognition()
|
|
103
112
|
this.imageButtonTarget?.removeEventListener('click', this.handleImageButtonClick)
|
|
104
113
|
this.imageInputTarget?.removeEventListener('change', this.handleImageChange)
|
|
114
|
+
this.textareaTarget.removeEventListener('input', this._autoResize)
|
|
105
115
|
this.textareaTarget.removeEventListener('dragover', this.handleDragOver)
|
|
106
116
|
this.textareaTarget.removeEventListener('drop', this.handleDrop)
|
|
107
117
|
this.textareaTarget.removeEventListener('paste', this.handlePaste)
|
|
@@ -132,8 +142,7 @@ export default class extends Controller {
|
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
onSelectionChanged({ size, moving }) {
|
|
135
|
-
|
|
136
|
-
this.moveButtonTarget.disabled = moving || size === 0
|
|
145
|
+
// Selection state now managed by list_controller action bar
|
|
137
146
|
}
|
|
138
147
|
|
|
139
148
|
focusTextarea() {
|
|
@@ -159,10 +168,14 @@ export default class extends Controller {
|
|
|
159
168
|
this.editingId = null
|
|
160
169
|
this.submitTarget.innerHTML = this.defaultSubmitHTML
|
|
161
170
|
this.submitTarget.disabled = false
|
|
171
|
+
this.submitTarget.classList.remove('review-submit-btn')
|
|
162
172
|
if (this.cancelTarget) this.cancelTarget.style.display = 'none'
|
|
163
173
|
this.presenceController?.clearManualTypingMessage()
|
|
164
174
|
this.clearImageAttachments()
|
|
165
175
|
this.cancelQuote()
|
|
176
|
+
this.textareaTarget.placeholder = ''
|
|
177
|
+
// Reset textarea height after clearing content
|
|
178
|
+
this.textareaTarget.style.height = 'auto'
|
|
166
179
|
}
|
|
167
180
|
|
|
168
181
|
setSendingState(isSending) {
|
|
@@ -184,19 +197,45 @@ export default class extends Controller {
|
|
|
184
197
|
|
|
185
198
|
handleSend(event) {
|
|
186
199
|
event.preventDefault()
|
|
200
|
+
|
|
201
|
+
// If active quote exists, handle based on type
|
|
202
|
+
const store = this._reviewStore
|
|
203
|
+
if (store.hasActive && !store.isEmpty) {
|
|
204
|
+
const activeQuote = store.activeQuote
|
|
205
|
+
if (activeQuote && activeQuote.type === 'question') {
|
|
206
|
+
store.saveActiveFeedback(this.textareaTarget.value)
|
|
207
|
+
this._sendQuestionQuote(activeQuote)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
this._commitActiveQuote()
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
187
214
|
const hasText = this.textareaTarget.value.trim().length > 0
|
|
215
|
+
const hasQuotes = !store.isEmpty
|
|
188
216
|
const hasImages = this.currentImageFiles().length > 0
|
|
189
|
-
if (this.sending || (!hasText && !hasImages) || !this.creativeId) return
|
|
217
|
+
if (this.sending || (!hasText && !hasQuotes && !hasImages) || !this.creativeId) return
|
|
190
218
|
this.sending = true
|
|
191
219
|
this.setSendingState(true)
|
|
192
220
|
this.presenceController?.stoppedTyping()
|
|
193
221
|
|
|
222
|
+
// Build final content from review quotes + user text
|
|
223
|
+
if (hasQuotes) {
|
|
224
|
+
store.backup(this.textareaTarget.value)
|
|
225
|
+
this.textareaTarget.value = store.buildContent(this.textareaTarget.value)
|
|
226
|
+
this._pendingReviewType = 'review'
|
|
227
|
+
}
|
|
228
|
+
|
|
194
229
|
const wasPrivate = this.privateCheckboxTarget?.checked ?? false
|
|
195
230
|
|
|
196
231
|
const formData = new FormData(this.formTarget)
|
|
197
232
|
if (this.currentTopicId) {
|
|
198
233
|
formData.append('comment[topic_id]', this.currentTopicId)
|
|
199
234
|
}
|
|
235
|
+
if (this._pendingReviewType) {
|
|
236
|
+
formData.append('comment[review_type]', this._pendingReviewType)
|
|
237
|
+
this._pendingReviewType = null
|
|
238
|
+
}
|
|
200
239
|
|
|
201
240
|
let url = `/creatives/${this.creativeId}/comments`
|
|
202
241
|
let method = 'POST'
|
|
@@ -205,13 +244,26 @@ export default class extends Controller {
|
|
|
205
244
|
method = 'PATCH'
|
|
206
245
|
}
|
|
207
246
|
|
|
208
|
-
fetch(url, {
|
|
247
|
+
const doFetch = () => fetch(url, {
|
|
209
248
|
method,
|
|
210
249
|
headers: { 'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content },
|
|
211
250
|
body: formData,
|
|
212
251
|
})
|
|
252
|
+
|
|
253
|
+
doFetch()
|
|
213
254
|
.then((response) => {
|
|
214
255
|
if (response.ok) return response.text()
|
|
256
|
+
// On 422, the CSRF token may have gone stale (e.g. after an OS
|
|
257
|
+
// window switch). Refresh the token and retry once before giving up.
|
|
258
|
+
if (response.status === 422 && !this._hasRetried) {
|
|
259
|
+
this._hasRetried = true
|
|
260
|
+
return refreshCsrfToken().then(() => doFetch()).then((retryResp) => {
|
|
261
|
+
if (retryResp.ok) return retryResp.text()
|
|
262
|
+
return retryResp.json().then((json) => {
|
|
263
|
+
throw new Error(json.errors?.join(', ') || 'Unable to save comment')
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
}
|
|
215
267
|
return response.json().then((json) => {
|
|
216
268
|
throw new Error(json.errors?.join(', ') || 'Unable to save comment')
|
|
217
269
|
})
|
|
@@ -257,9 +309,17 @@ export default class extends Controller {
|
|
|
257
309
|
}
|
|
258
310
|
})
|
|
259
311
|
.catch((error) => {
|
|
312
|
+
// Restore review quotes state on failure so user doesn't lose work
|
|
313
|
+
if (this._reviewStore.hasBackup()) {
|
|
314
|
+
const restoredText = this._reviewStore.restore()
|
|
315
|
+
this.textareaTarget.value = restoredText || ''
|
|
316
|
+
this._renderReviewQuoteChips()
|
|
317
|
+
this._updateSubmitButton()
|
|
318
|
+
}
|
|
260
319
|
alert(error?.message || 'Failed to submit comment')
|
|
261
320
|
})
|
|
262
321
|
.finally(() => {
|
|
322
|
+
this._hasRetried = false
|
|
263
323
|
this.setSendingState(false)
|
|
264
324
|
})
|
|
265
325
|
}
|
|
@@ -280,11 +340,6 @@ export default class extends Controller {
|
|
|
280
340
|
this.resetForm()
|
|
281
341
|
}
|
|
282
342
|
|
|
283
|
-
handleMoveClick(event) {
|
|
284
|
-
event.preventDefault()
|
|
285
|
-
this.listController?.openMoveModal()
|
|
286
|
-
}
|
|
287
|
-
|
|
288
343
|
handleSearch(event) {
|
|
289
344
|
event.preventDefault()
|
|
290
345
|
const query = this.textareaTarget.value.trim()
|
|
@@ -544,11 +599,247 @@ export default class extends Controller {
|
|
|
544
599
|
this.focusTextarea()
|
|
545
600
|
}
|
|
546
601
|
|
|
602
|
+
// Append a review quote as a visual chip above the textarea.
|
|
603
|
+
appendReviewQuote(commentId, selectedText) {
|
|
604
|
+
if (!selectedText) return
|
|
605
|
+
|
|
606
|
+
const store = this._reviewStore
|
|
607
|
+
store.saveActiveFeedback(this.textareaTarget.value)
|
|
608
|
+
|
|
609
|
+
if (commentId) {
|
|
610
|
+
this.quotedCommentIdTarget.value = commentId
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
store.add(commentId, selectedText)
|
|
614
|
+
this._renderReviewQuoteChips()
|
|
615
|
+
this._updateSubmitButton()
|
|
616
|
+
|
|
617
|
+
this.textareaTarget.value = ''
|
|
618
|
+
this.textareaTarget.placeholder = this._getI18nText('reviewFeedbackPlaceholder', 'Write feedback for this quote...')
|
|
619
|
+
this.focusTextarea()
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
_commitActiveQuote() {
|
|
623
|
+
const store = this._reviewStore
|
|
624
|
+
store.commitActive(this.textareaTarget.value)
|
|
625
|
+
this.textareaTarget.value = ''
|
|
626
|
+
this.textareaTarget.placeholder = this._getI18nText('reviewSummaryPlaceholder', 'Overall comment (optional)...')
|
|
627
|
+
this._renderReviewQuoteChips()
|
|
628
|
+
this._updateSubmitButton()
|
|
629
|
+
this.focusTextarea()
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Send a single question quote immediately as a standalone comment.
|
|
633
|
+
_sendQuestionQuote(quote) {
|
|
634
|
+
if (this.sending || !this.creativeId) return
|
|
635
|
+
|
|
636
|
+
const store = this._reviewStore
|
|
637
|
+
const content = store.buildQuestionContent(quote)
|
|
638
|
+
|
|
639
|
+
store.remove(quote.id)
|
|
640
|
+
this.textareaTarget.value = ''
|
|
641
|
+
|
|
642
|
+
if (store.isEmpty) {
|
|
643
|
+
this.textareaTarget.placeholder = ''
|
|
644
|
+
} else if (!store.hasActive) {
|
|
645
|
+
this.textareaTarget.placeholder = this._getI18nText('reviewSummaryPlaceholder', 'Overall comment (optional)...')
|
|
646
|
+
}
|
|
647
|
+
this._renderReviewQuoteChips()
|
|
648
|
+
this._updateSubmitButton()
|
|
649
|
+
|
|
650
|
+
// Send as a question comment with quoted_comment_id preserved + review_type=question
|
|
651
|
+
this.sending = true
|
|
652
|
+
const formData = new FormData()
|
|
653
|
+
formData.append('comment[content]', content)
|
|
654
|
+
formData.append('comment[review_type]', 'question')
|
|
655
|
+
if (quote.commentId) {
|
|
656
|
+
formData.append('comment[quoted_comment_id]', quote.commentId)
|
|
657
|
+
}
|
|
658
|
+
const isPrivate = this.privateCheckboxTarget?.checked ?? false
|
|
659
|
+
if (isPrivate) formData.append('comment[private]', '1')
|
|
660
|
+
if (this.currentTopicId) {
|
|
661
|
+
formData.append('comment[topic_id]', this.currentTopicId)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const url = `/creatives/${this.creativeId}/comments`
|
|
665
|
+
const doFetch = () => fetch(url, {
|
|
666
|
+
method: 'POST',
|
|
667
|
+
headers: { 'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content },
|
|
668
|
+
body: formData,
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
doFetch()
|
|
672
|
+
.then((response) => {
|
|
673
|
+
if (response.ok) return response.text()
|
|
674
|
+
if (response.status === 422 && !this._hasRetried) {
|
|
675
|
+
this._hasRetried = true
|
|
676
|
+
return refreshCsrfToken().then(() => doFetch()).then((retryResp) => {
|
|
677
|
+
if (retryResp.ok) return retryResp.text()
|
|
678
|
+
return retryResp.json().then((json) => {
|
|
679
|
+
throw new Error(json.errors?.join(', ') || 'Unable to save comment')
|
|
680
|
+
})
|
|
681
|
+
})
|
|
682
|
+
}
|
|
683
|
+
return response.json().then((json) => {
|
|
684
|
+
throw new Error(json.errors?.join(', ') || 'Unable to save comment')
|
|
685
|
+
})
|
|
686
|
+
})
|
|
687
|
+
.then((html) => {
|
|
688
|
+
this.renderCommentHtml(html)
|
|
689
|
+
const listCtrl = this.application.getControllerForElementAndIdentifier(
|
|
690
|
+
document.querySelector('[data-controller~="comments--list"]'), 'comments--list'
|
|
691
|
+
)
|
|
692
|
+
if (listCtrl) {
|
|
693
|
+
listCtrl.scrollToBottom()
|
|
694
|
+
listCtrl.updateStickiness()
|
|
695
|
+
listCtrl.markCommentsRead()
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
.catch((error) => {
|
|
699
|
+
alert(error?.message || 'Failed to send question')
|
|
700
|
+
})
|
|
701
|
+
.finally(() => {
|
|
702
|
+
this._hasRetried = false
|
|
703
|
+
this.sending = false
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
this.focusTextarea()
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
_renderReviewQuoteChips() {
|
|
710
|
+
const container = this.reviewQuotesContainerTarget
|
|
711
|
+
const store = this._reviewStore
|
|
712
|
+
container.innerHTML = ''
|
|
713
|
+
|
|
714
|
+
if (store.isEmpty) {
|
|
715
|
+
container.style.display = 'none'
|
|
716
|
+
return
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
container.style.display = ''
|
|
720
|
+
store.quotes.forEach((quote) => {
|
|
721
|
+
const chip = document.createElement('div')
|
|
722
|
+
chip.className = 'review-quote-chip'
|
|
723
|
+
if (quote.id === store.activeId) {
|
|
724
|
+
chip.classList.add('review-quote-chip--active')
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Type toggle (review / question)
|
|
728
|
+
const typeToggle = document.createElement('button')
|
|
729
|
+
typeToggle.type = 'button'
|
|
730
|
+
typeToggle.className = `review-quote-type-toggle review-quote-type-toggle--${quote.type}`
|
|
731
|
+
typeToggle.textContent = quote.type === 'question'
|
|
732
|
+
? this._getI18nText('reviewTypeQuestion', '❓')
|
|
733
|
+
: this._getI18nText('reviewTypeReview', '💬')
|
|
734
|
+
typeToggle.title = quote.type === 'question'
|
|
735
|
+
? this._getI18nText('reviewTypeQuestionLabel', 'Question')
|
|
736
|
+
: this._getI18nText('reviewTypeReviewLabel', 'Review')
|
|
737
|
+
typeToggle.addEventListener('click', (e) => {
|
|
738
|
+
e.stopPropagation()
|
|
739
|
+
store.toggleType(quote.id)
|
|
740
|
+
this._renderReviewQuoteChips()
|
|
741
|
+
this._updateSubmitButton()
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
// Quote text
|
|
745
|
+
const textSpan = document.createElement('span')
|
|
746
|
+
textSpan.className = 'review-quote-chip-text'
|
|
747
|
+
const preview = quote.text.length > 60
|
|
748
|
+
? quote.text.substring(0, 60) + '…'
|
|
749
|
+
: quote.text
|
|
750
|
+
textSpan.textContent = preview
|
|
751
|
+
textSpan.title = quote.text
|
|
752
|
+
|
|
753
|
+
// Click chip to edit its feedback
|
|
754
|
+
textSpan.addEventListener('click', () => {
|
|
755
|
+
if (quote.id === store.activeId) {
|
|
756
|
+
const commentEl = document.querySelector(`[data-comment-id="${quote.commentId}"]`)
|
|
757
|
+
if (commentEl) {
|
|
758
|
+
commentEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
759
|
+
commentEl.classList.add('comment-highlight')
|
|
760
|
+
setTimeout(() => commentEl.classList.remove('comment-highlight'), 2000)
|
|
761
|
+
}
|
|
762
|
+
return
|
|
763
|
+
}
|
|
764
|
+
store.saveActiveFeedback(this.textareaTarget.value)
|
|
765
|
+
store.activate(quote.id)
|
|
766
|
+
this.textareaTarget.value = quote.feedback || ''
|
|
767
|
+
this.textareaTarget.placeholder = this._getI18nText('reviewFeedbackPlaceholder', 'Write feedback for this quote...')
|
|
768
|
+
this._renderReviewQuoteChips()
|
|
769
|
+
this._updateSubmitButton()
|
|
770
|
+
this.focusTextarea()
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
// Feedback preview (shown when not active and has feedback)
|
|
774
|
+
const feedbackSpan = document.createElement('span')
|
|
775
|
+
feedbackSpan.className = 'review-quote-chip-feedback'
|
|
776
|
+
if (quote.feedback && quote.id !== store.activeId) {
|
|
777
|
+
const fbPreview = quote.feedback.length > 40
|
|
778
|
+
? quote.feedback.substring(0, 40) + '…'
|
|
779
|
+
: quote.feedback
|
|
780
|
+
feedbackSpan.textContent = `→ ${fbPreview}`
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Remove button
|
|
784
|
+
const removeBtn = document.createElement('button')
|
|
785
|
+
removeBtn.type = 'button'
|
|
786
|
+
removeBtn.className = 'review-quote-chip-remove'
|
|
787
|
+
removeBtn.innerHTML = '×'
|
|
788
|
+
removeBtn.title = 'Remove'
|
|
789
|
+
removeBtn.addEventListener('click', (e) => {
|
|
790
|
+
e.stopPropagation()
|
|
791
|
+
const wasActive = quote.id === store.activeId
|
|
792
|
+
store.remove(quote.id)
|
|
793
|
+
if (wasActive) this.textareaTarget.value = ''
|
|
794
|
+
if (store.isEmpty) {
|
|
795
|
+
this.textareaTarget.placeholder = ''
|
|
796
|
+
} else if (!store.hasActive) {
|
|
797
|
+
this.textareaTarget.placeholder = this._getI18nText('reviewSummaryPlaceholder', 'Overall comment (optional)...')
|
|
798
|
+
}
|
|
799
|
+
this._renderReviewQuoteChips()
|
|
800
|
+
this._updateSubmitButton()
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
chip.appendChild(typeToggle)
|
|
804
|
+
chip.appendChild(textSpan)
|
|
805
|
+
if (quote.feedback && quote.id !== store.activeId) {
|
|
806
|
+
chip.appendChild(feedbackSpan)
|
|
807
|
+
}
|
|
808
|
+
chip.appendChild(removeBtn)
|
|
809
|
+
container.appendChild(chip)
|
|
810
|
+
})
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
_updateSubmitButton() {
|
|
814
|
+
const state = this._reviewStore.buttonState
|
|
815
|
+
if (state === 'normal') {
|
|
816
|
+
this.submitTarget.innerHTML = this.defaultSubmitHTML
|
|
817
|
+
this.submitTarget.classList.remove('review-submit-btn')
|
|
818
|
+
return
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
this.submitTarget.classList.add('review-submit-btn')
|
|
822
|
+
const labels = {
|
|
823
|
+
'add': this._getI18nText('reviewAddQuote', '+ Add'),
|
|
824
|
+
'send-review': this._getI18nText('reviewSend', 'Send review'),
|
|
825
|
+
'send-question': this._getI18nText('reviewSendQuestion', 'Send question'),
|
|
826
|
+
}
|
|
827
|
+
this.submitTarget.textContent = labels[state]
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
_getI18nText(key, fallback) {
|
|
831
|
+
return this.element.dataset[key] || fallback
|
|
832
|
+
}
|
|
833
|
+
|
|
547
834
|
cancelQuote() {
|
|
548
835
|
this.quotedCommentIdTarget.value = ''
|
|
549
836
|
this.quotedTextTarget.value = ''
|
|
550
837
|
this.quoteIndicatorTarget.style.display = 'none'
|
|
551
838
|
this.quoteIndicatorTextTarget.textContent = ''
|
|
839
|
+
this._reviewStore.clear()
|
|
840
|
+
this._renderReviewQuoteChips()
|
|
841
|
+
this._updateSubmitButton()
|
|
842
|
+
this.textareaTarget.placeholder = ''
|
|
552
843
|
}
|
|
553
844
|
|
|
554
845
|
renderCommentHtml(html, { replaceExisting = false } = {}) {
|