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
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["list", "toggleButton"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this.contexts = []
|
|
8
|
+
this.canManage = false
|
|
9
|
+
this.draggingContextId = null
|
|
10
|
+
this.listVisible = false
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get creativeId() {
|
|
14
|
+
return this.element.closest('#comments-popup')?.dataset?.creativeId
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async onPopupOpened({ creativeId }) {
|
|
18
|
+
this._hasBeenManuallyToggled = false
|
|
19
|
+
this.listVisible = false
|
|
20
|
+
this._updateListVisibility()
|
|
21
|
+
await this.loadContexts()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
onPopupClosed() {
|
|
25
|
+
this.contexts = []
|
|
26
|
+
this.canManage = false
|
|
27
|
+
if (this.hasListTarget) {
|
|
28
|
+
this.listTarget.innerHTML = ''
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async loadContexts() {
|
|
33
|
+
const creativeId = this.creativeId
|
|
34
|
+
if (!creativeId) return
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(`/creatives/${creativeId}/contexts`)
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
const data = await response.json()
|
|
40
|
+
this.contexts = data.contexts || []
|
|
41
|
+
this.canManage = data.can_manage || false
|
|
42
|
+
this._selfContextDisabled = data.disabled_self_context || false
|
|
43
|
+
this.renderContexts()
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error("Failed to load contexts", e)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
toggleVisibility() {
|
|
51
|
+
this._hasBeenManuallyToggled = true
|
|
52
|
+
this.listVisible = !this.listVisible
|
|
53
|
+
this._updateListVisibility()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_updateListVisibility() {
|
|
57
|
+
if (!this.hasListTarget) return
|
|
58
|
+
this.listTarget.style.display = this.listVisible ? '' : 'none'
|
|
59
|
+
|
|
60
|
+
// Update toggle button active state
|
|
61
|
+
if (this.hasToggleButtonTarget) {
|
|
62
|
+
this.toggleButtonTarget.classList.toggle('context-toggle-active', this.listVisible)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_updateToggleButton() {
|
|
67
|
+
if (!this.hasToggleButtonTarget) return
|
|
68
|
+
|
|
69
|
+
const hasLinkedContexts = this.contexts.length > 0
|
|
70
|
+
// Always show button (self-context toggle is always available)
|
|
71
|
+
this.toggleButtonTarget.style.display = ''
|
|
72
|
+
|
|
73
|
+
// Show badge count when hidden
|
|
74
|
+
if (!this.listVisible) {
|
|
75
|
+
const activeLinked = this.contexts.filter(c => !c.disabled).length
|
|
76
|
+
const selfActive = this.selfContextDisabled ? 0 : 1
|
|
77
|
+
const total = activeLinked + selfActive
|
|
78
|
+
this.toggleButtonTarget.textContent = `🔗 ${total}`
|
|
79
|
+
} else {
|
|
80
|
+
this.toggleButtonTarget.textContent = '🔗'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Auto-show if linked contexts exist, otherwise keep hidden
|
|
84
|
+
if (hasLinkedContexts && !this._hasBeenManuallyToggled) {
|
|
85
|
+
this.listVisible = true
|
|
86
|
+
this._updateListVisibility()
|
|
87
|
+
} else if (!hasLinkedContexts && !this.canManage && !this._hasBeenManuallyToggled) {
|
|
88
|
+
this.listVisible = false
|
|
89
|
+
this._updateListVisibility()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
renderContexts() {
|
|
94
|
+
if (!this.hasListTarget) return
|
|
95
|
+
|
|
96
|
+
this._updateToggleButton()
|
|
97
|
+
|
|
98
|
+
const dragActions = this.canManage
|
|
99
|
+
? 'dragstart->comments--contexts#handleDragStart dragend->comments--contexts#handleDragEnd'
|
|
100
|
+
: ''
|
|
101
|
+
const reorderActions = this.canManage
|
|
102
|
+
? 'dragover->comments--contexts#handleReorderDragOver dragleave->comments--contexts#handleReorderDragLeave drop->comments--contexts#handleReorderDrop'
|
|
103
|
+
: ''
|
|
104
|
+
|
|
105
|
+
let html = ''
|
|
106
|
+
|
|
107
|
+
// Current creative self-context toggle (always first)
|
|
108
|
+
const selfDisabled = this.selfContextDisabled
|
|
109
|
+
const selfClass = selfDisabled ? 'context-disabled' : ''
|
|
110
|
+
const selfLabel = this._escapeHtml(this.currentCreativeSnippet || 'Self')
|
|
111
|
+
html += `<span class="context-chip context-self ${selfClass}"
|
|
112
|
+
data-action="click->comments--contexts#toggleSelfContext"
|
|
113
|
+
title="${this.selfContextLabel}">
|
|
114
|
+
📌 ${selfLabel}
|
|
115
|
+
</span>`
|
|
116
|
+
|
|
117
|
+
this.contexts.forEach(ctx => {
|
|
118
|
+
const disabledClass = ctx.disabled ? 'context-disabled' : ''
|
|
119
|
+
const inheritedClass = ctx.inherited ? 'context-inherited' : ''
|
|
120
|
+
const draggable = this.canManage && !ctx.inherited ? 'draggable="true"' : ''
|
|
121
|
+
|
|
122
|
+
html += `<span class="context-chip ${disabledClass} ${inheritedClass}" ${draggable}
|
|
123
|
+
data-action="click->comments--contexts#toggleContext ${dragActions} ${reorderActions}"
|
|
124
|
+
data-context-id="${ctx.id}"
|
|
125
|
+
title="${ctx.inherited ? this.inheritedLabel : ''}">
|
|
126
|
+
🔗 ${this._escapeHtml(ctx.description)}`
|
|
127
|
+
|
|
128
|
+
html += `<button class="navigate-context-btn" data-action="click->comments--contexts#navigateToContext" data-context-id="${ctx.id}" title="${this.navigateLabel}">\u2192</button>`
|
|
129
|
+
|
|
130
|
+
if (this.canManage && !ctx.inherited) {
|
|
131
|
+
html += `<button class="delete-context-btn" data-action="click->comments--contexts#removeContext" data-context-id="${ctx.id}">×</button>`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
html += `</span>`
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
if (this.canManage) {
|
|
138
|
+
html += `<button class="add-context-btn" data-action="click->comments--contexts#addContext">+</button>`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.listTarget.innerHTML = html
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get inheritedLabel() {
|
|
145
|
+
return this.listTarget.dataset.inheritedLabel || 'Inherited from parent'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get selfContextLabel() {
|
|
149
|
+
return this.listTarget.dataset.selfContextLabel || 'Current creative context'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get navigateLabel() {
|
|
153
|
+
return this.listTarget.dataset.navigateLabel || 'Go to creative'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get currentCreativeSnippet() {
|
|
157
|
+
const popup = this.element.closest('#comments-popup')
|
|
158
|
+
return popup?.querySelector('#comments-popup-title')?.textContent?.trim()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get selfContextDisabled() {
|
|
162
|
+
return this._selfContextDisabled || false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
toggleSelfContext(event) {
|
|
166
|
+
this._selfContextDisabled = !this._selfContextDisabled
|
|
167
|
+
this.renderContexts()
|
|
168
|
+
this._saveSelfContextState()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async _saveSelfContextState() {
|
|
172
|
+
await this._patchContexts({ disabled_self_context: this._selfContextDisabled })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
navigateToContext(event) {
|
|
176
|
+
event.stopPropagation()
|
|
177
|
+
const contextId = event.currentTarget.dataset.contextId
|
|
178
|
+
if (!contextId) return
|
|
179
|
+
window.location.href = `/creatives/${contextId}`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
toggleContext(event) {
|
|
183
|
+
if (event.target.closest('.delete-context-btn') || event.target.closest('.navigate-context-btn')) return
|
|
184
|
+
|
|
185
|
+
const contextId = parseInt(event.currentTarget.dataset.contextId)
|
|
186
|
+
if (!contextId) return
|
|
187
|
+
|
|
188
|
+
const ctx = this.contexts.find(c => c.id === contextId)
|
|
189
|
+
if (!ctx) return
|
|
190
|
+
|
|
191
|
+
ctx.disabled = !ctx.disabled
|
|
192
|
+
this.renderContexts()
|
|
193
|
+
this._saveDisabledState()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async removeContext(event) {
|
|
197
|
+
event.stopPropagation()
|
|
198
|
+
const contextId = parseInt(event.currentTarget.dataset.contextId)
|
|
199
|
+
if (!contextId) return
|
|
200
|
+
|
|
201
|
+
const ownContexts = this.contexts.filter(c => !c.inherited)
|
|
202
|
+
const newIds = ownContexts.filter(c => c.id !== contextId).map(c => c.id)
|
|
203
|
+
|
|
204
|
+
await this._updateContextIds(newIds)
|
|
205
|
+
await this.loadContexts()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
addContext() {
|
|
209
|
+
// Use the existing link-creative modal
|
|
210
|
+
const linkController = window.Stimulus?.getControllerForElementAndIdentifier(
|
|
211
|
+
document.querySelector('[data-controller~="link-creative"]'),
|
|
212
|
+
'link-creative'
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if (!linkController) {
|
|
216
|
+
console.error("link-creative controller not found")
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const addBtn = this.listTarget.querySelector('.add-context-btn')
|
|
221
|
+
const rect = addBtn?.getBoundingClientRect() || { top: 200, left: 200, bottom: 230, right: 230 }
|
|
222
|
+
|
|
223
|
+
linkController.open(rect, (selectedCreative) => {
|
|
224
|
+
this._addContextId(selectedCreative.id)
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async _addContextId(creativeId) {
|
|
229
|
+
// Prevent adding self as context
|
|
230
|
+
const selfId = parseInt(this.creativeId)
|
|
231
|
+
if (creativeId === selfId) return
|
|
232
|
+
|
|
233
|
+
const ownContexts = this.contexts.filter(c => !c.inherited)
|
|
234
|
+
const existingIds = ownContexts.map(c => c.id)
|
|
235
|
+
|
|
236
|
+
if (existingIds.includes(creativeId)) return
|
|
237
|
+
// Also check inherited
|
|
238
|
+
if (this.contexts.some(c => c.id === creativeId)) return
|
|
239
|
+
|
|
240
|
+
const newIds = [...existingIds, creativeId]
|
|
241
|
+
await this._updateContextIds(newIds)
|
|
242
|
+
await this.loadContexts()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// --- Drag & Drop for reordering ---
|
|
246
|
+
handleDragStart(event) {
|
|
247
|
+
const chipEl = event.currentTarget
|
|
248
|
+
const contextId = chipEl.dataset.contextId
|
|
249
|
+
if (!contextId) { event.preventDefault(); return }
|
|
250
|
+
|
|
251
|
+
this.draggingContextId = contextId
|
|
252
|
+
event.dataTransfer.setData('application/x-context-id', contextId)
|
|
253
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
254
|
+
|
|
255
|
+
requestAnimationFrame(() => {
|
|
256
|
+
chipEl.classList.add('context-dragging')
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
handleDragEnd(event) {
|
|
261
|
+
this.draggingContextId = null
|
|
262
|
+
event.currentTarget.classList.remove('context-dragging')
|
|
263
|
+
this.listTarget.querySelectorAll('.context-chip').forEach(el => {
|
|
264
|
+
el.classList.remove('context-drag-over-left', 'context-drag-over-right')
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
handleReorderDragOver(event) {
|
|
269
|
+
if (!event.dataTransfer.types.includes('application/x-context-id')) return
|
|
270
|
+
if (!this.draggingContextId) return
|
|
271
|
+
|
|
272
|
+
const targetEl = event.currentTarget
|
|
273
|
+
const targetId = targetEl.dataset.contextId
|
|
274
|
+
if (!targetId || targetId === this.draggingContextId) return
|
|
275
|
+
// Don't allow reorder onto inherited chips
|
|
276
|
+
const ctx = this.contexts.find(c => String(c.id) === targetId)
|
|
277
|
+
if (ctx?.inherited) return
|
|
278
|
+
|
|
279
|
+
event.preventDefault()
|
|
280
|
+
event.dataTransfer.dropEffect = 'move'
|
|
281
|
+
|
|
282
|
+
const rect = targetEl.getBoundingClientRect()
|
|
283
|
+
const midpoint = rect.left + rect.width / 2
|
|
284
|
+
const isLeft = event.clientX < midpoint
|
|
285
|
+
|
|
286
|
+
targetEl.classList.toggle('context-drag-over-left', isLeft)
|
|
287
|
+
targetEl.classList.toggle('context-drag-over-right', !isLeft)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
handleReorderDragLeave(event) {
|
|
291
|
+
event.currentTarget.classList.remove('context-drag-over-left', 'context-drag-over-right')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async handleReorderDrop(event) {
|
|
295
|
+
event.preventDefault()
|
|
296
|
+
|
|
297
|
+
const targetEl = event.currentTarget
|
|
298
|
+
targetEl.classList.remove('context-drag-over-left', 'context-drag-over-right')
|
|
299
|
+
|
|
300
|
+
const draggedId = parseInt(event.dataTransfer.getData('application/x-context-id'))
|
|
301
|
+
const targetId = parseInt(targetEl.dataset.contextId)
|
|
302
|
+
|
|
303
|
+
if (!draggedId || !targetId || draggedId === targetId) return
|
|
304
|
+
|
|
305
|
+
const ownContexts = this.contexts.filter(c => !c.inherited)
|
|
306
|
+
const ids = ownContexts.map(c => c.id)
|
|
307
|
+
|
|
308
|
+
const draggedIndex = ids.indexOf(draggedId)
|
|
309
|
+
const targetIndex = ids.indexOf(targetId)
|
|
310
|
+
if (draggedIndex === -1 || targetIndex === -1) return
|
|
311
|
+
|
|
312
|
+
// Determine drop position
|
|
313
|
+
const rect = targetEl.getBoundingClientRect()
|
|
314
|
+
const midpoint = rect.left + rect.width / 2
|
|
315
|
+
const insertBefore = event.clientX < midpoint
|
|
316
|
+
|
|
317
|
+
ids.splice(draggedIndex, 1)
|
|
318
|
+
let newIndex = ids.indexOf(targetId)
|
|
319
|
+
if (!insertBefore) newIndex += 1
|
|
320
|
+
ids.splice(newIndex, 0, draggedId)
|
|
321
|
+
|
|
322
|
+
await this._updateContextIds(ids)
|
|
323
|
+
await this.loadContexts()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// --- API calls ---
|
|
327
|
+
async _updateContextIds(ids) {
|
|
328
|
+
await this._patchContexts({ context_ids: ids })
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async _saveDisabledState() {
|
|
332
|
+
const disabledIds = this.contexts.filter(c => c.disabled).map(c => c.id)
|
|
333
|
+
await this._patchContexts({ disabled_context_ids: disabledIds })
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async _patchContexts(params) {
|
|
337
|
+
const creativeId = this.creativeId
|
|
338
|
+
if (!creativeId) return
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const response = await fetch(`/creatives/${creativeId}/update_contexts`, {
|
|
342
|
+
method: 'PATCH',
|
|
343
|
+
headers: {
|
|
344
|
+
'Content-Type': 'application/json',
|
|
345
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
|
346
|
+
},
|
|
347
|
+
body: JSON.stringify(params)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
if (!response.ok) {
|
|
351
|
+
console.error('Failed to update contexts', params)
|
|
352
|
+
}
|
|
353
|
+
} catch (e) {
|
|
354
|
+
console.error('Error updating contexts', e)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_escapeHtml(text) {
|
|
359
|
+
const div = document.createElement('div')
|
|
360
|
+
div.textContent = text
|
|
361
|
+
return div.innerHTML
|
|
362
|
+
}
|
|
363
|
+
}
|