collavre 0.22.0 → 0.24.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/README.md +1 -0
- data/app/assets/images/collavre/landing/interface-en-dark.png +0 -0
- data/app/assets/images/collavre/landing/interface-en.png +0 -0
- data/app/assets/images/collavre/landing/interface-ko-dark.png +0 -0
- data/app/assets/images/collavre/landing/interface-ko.png +0 -0
- data/app/assets/stylesheets/collavre/actiontext.css +251 -90
- data/app/assets/stylesheets/collavre/code_highlight.css +7 -201
- data/app/assets/stylesheets/collavre/comments_popup.css +158 -61
- data/app/assets/stylesheets/collavre/creatives.css +11 -2
- data/app/assets/stylesheets/collavre/landing.css +92 -0
- data/app/assets/stylesheets/collavre/modal_dialog.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +4 -2
- data/app/assets/stylesheets/collavre/tables.css +91 -0
- data/app/channels/collavre/inbox_badge_channel.rb +30 -0
- data/app/controllers/collavre/admin/settings_controller.rb +52 -67
- data/app/controllers/collavre/api/v1/agents_controller.rb +17 -188
- data/app/controllers/collavre/api/v1/base_controller.rb +9 -27
- data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +224 -0
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +95 -0
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +31 -0
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +25 -0
- data/app/controllers/collavre/attachments_controller.rb +13 -3
- data/app/controllers/collavre/concerns/slide_viewable.rb +8 -32
- data/app/controllers/collavre/creatives_controller.rb +47 -103
- data/app/controllers/collavre/inbox_items_controller.rb +10 -4
- data/app/controllers/collavre/landing_controller.rb +5 -2
- data/app/controllers/collavre/tasks_controller.rb +13 -4
- data/app/controllers/collavre/topics_controller.rb +54 -1
- data/app/controllers/collavre/typo_corrections_controller.rb +39 -0
- data/app/controllers/concerns/collavre/api/authenticatable.rb +40 -0
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +16 -1
- data/app/controllers/concerns/collavre/users_controller/registration.rb +41 -1
- data/app/errors/collavre/api_error.rb +20 -0
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/helpers/collavre/creatives_helper.rb +30 -8
- data/app/javascript/collavre.js +2 -0
- data/app/javascript/components/ImageResizer.jsx +9 -3
- data/app/javascript/components/InlineLexicalEditor.jsx +157 -72
- data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +87 -0
- data/app/javascript/components/creative_tree_row.js +28 -5
- data/app/javascript/components/plugins/list_tab_indent_plugin.jsx +16 -0
- data/app/javascript/components/plugins/markdown_shortcuts_plugin.jsx +15 -4
- data/app/javascript/components/plugins/table_hover_actions_plugin.jsx +405 -0
- data/app/javascript/controllers/__tests__/inbox_badge_controller.test.js +73 -0
- data/app/javascript/controllers/__tests__/search_popup_controller.test.js +104 -0
- data/app/javascript/controllers/__tests__/topic_list_controller.test.js +82 -0
- data/app/javascript/controllers/comment_controller.js +5 -4
- data/app/javascript/controllers/comment_version_controller.js +2 -1
- data/app/javascript/controllers/comments/__tests__/form_controller_double_submit.test.js +159 -0
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +3 -2
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +94 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +61 -0
- data/app/javascript/controllers/comments/form_controller.js +21 -5
- data/app/javascript/controllers/comments/list_controller.js +18 -17
- data/app/javascript/controllers/comments/presence_controller.js +2 -1
- data/app/javascript/controllers/comments/topics_controller.js +61 -8
- data/app/javascript/controllers/common_popup_controller.js +2 -2
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +150 -0
- data/app/javascript/controllers/creatives/import_controller.js +2 -1
- data/app/javascript/controllers/creatives/select_mode_controller.js +2 -1
- data/app/javascript/controllers/creatives/tree_controller.js +142 -1
- data/app/javascript/controllers/image_lightbox_controller.js +2 -1
- data/app/javascript/controllers/inbox_badge_controller.js +33 -0
- data/app/javascript/controllers/index.js +7 -1
- data/app/javascript/controllers/search_popup_controller.js +17 -1
- data/app/javascript/controllers/share_modal_controller.js +4 -3
- data/app/javascript/controllers/topic_list_controller.js +78 -0
- data/app/javascript/controllers/topic_search_controller.js +2 -1
- data/app/javascript/creatives/drag_drop/event_handlers.js +14 -5
- data/app/javascript/creatives/topic_move_members_popup.js +156 -0
- data/app/javascript/creatives/tree_renderer.js +11 -0
- data/app/javascript/lib/__tests__/common_popup_bounds.test.js +58 -0
- data/app/javascript/lib/__tests__/turbo_confirm.test.js +81 -0
- data/app/javascript/lib/__tests__/typo_correction.test.js +192 -0
- data/app/javascript/lib/api/__tests__/api_error.test.js +96 -0
- data/app/javascript/lib/api/__tests__/queue_manager.test.js +88 -1
- data/app/javascript/lib/api/api_error.js +108 -0
- data/app/javascript/lib/api/queue_manager.js +38 -4
- data/app/javascript/lib/common_popup.js +42 -10
- data/app/javascript/lib/editor/__tests__/code_edit_view_token_parity.test.js +121 -0
- data/app/javascript/lib/editor/__tests__/code_language_roundtrip.test.js +152 -0
- data/app/javascript/lib/editor/__tests__/code_languages.test.js +93 -0
- data/app/javascript/lib/editor/code_languages.js +173 -0
- data/app/javascript/lib/editor/code_token_theme.js +41 -0
- data/app/javascript/lib/lexical/__tests__/code_block_toggle.test.js +326 -0
- data/app/javascript/lib/lexical/__tests__/code_fence_shortcut.test.js +179 -0
- data/app/javascript/lib/lexical/__tests__/image_focus.test.js +139 -0
- data/app/javascript/lib/lexical/__tests__/list_tab_indent.test.js +633 -0
- data/app/javascript/lib/lexical/__tests__/markdown_serialize.test.js +627 -0
- data/app/javascript/lib/lexical/__tests__/minimize_html.test.js +20 -1
- data/app/javascript/lib/lexical/__tests__/selection_boundary.test.js +88 -0
- data/app/javascript/lib/lexical/__tests__/table_transformer.test.js +163 -0
- data/app/javascript/lib/lexical/__tests__/trailing_paragraph.test.js +104 -0
- data/app/javascript/lib/lexical/code_block_toggle.js +130 -0
- data/app/javascript/lib/lexical/code_fence_shortcut.js +60 -0
- data/app/javascript/lib/lexical/list_tab_indent.js +210 -0
- data/app/javascript/lib/lexical/markdown_serialize.js +320 -0
- data/app/javascript/lib/lexical/selection_boundary.js +58 -0
- data/app/javascript/lib/lexical/table_transformer.js +182 -0
- data/app/javascript/lib/lexical/trailing_paragraph.js +29 -0
- data/app/javascript/lib/turbo_confirm.js +46 -0
- data/app/javascript/lib/typo_correction.js +146 -0
- data/app/javascript/lib/utils/__tests__/confirm_dialog.test.js +88 -0
- data/app/javascript/lib/utils/__tests__/dialog.test.js +92 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +153 -0
- data/app/javascript/lib/utils/__tests__/sanitize_description.test.js +68 -0
- data/app/javascript/lib/utils/__tests__/table_download.test.js +93 -0
- data/app/javascript/lib/utils/confirm_dialog.js +10 -0
- data/app/javascript/lib/utils/dialog.js +300 -0
- data/app/javascript/lib/utils/markdown.js +154 -67
- data/app/javascript/lib/utils/sanitize_description.js +31 -0
- data/app/javascript/lib/utils/table_download.js +15 -0
- data/app/javascript/modules/__tests__/creative_inline_payload.test.js +231 -0
- data/app/javascript/modules/__tests__/creative_save_queue.test.js +228 -0
- data/app/javascript/modules/__tests__/creative_tree_dom.test.js +425 -0
- data/app/javascript/modules/__tests__/typo_corrector.test.js +365 -0
- data/app/javascript/modules/creative_inline_payload.js +86 -0
- data/app/javascript/modules/creative_row_editor.js +231 -471
- data/app/javascript/modules/creative_row_editor_helpers.js +76 -0
- data/app/javascript/modules/creative_save_queue.js +95 -0
- data/app/javascript/modules/creative_tree_dom.js +260 -0
- data/app/javascript/modules/export_to_markdown.js +2 -1
- data/app/javascript/modules/lexical_inline_editor.jsx +2 -1
- data/app/javascript/modules/slide_view.js +11 -2
- data/app/javascript/modules/typo_corrector.js +534 -0
- data/app/jobs/collavre/ai_agent_job.rb +7 -4
- data/app/jobs/collavre/compress_job.rb +6 -2
- data/app/jobs/collavre/permission_cache_job.rb +20 -2
- data/app/jobs/collavre/trigger_loop_check_job.rb +20 -3
- data/app/jobs/collavre/update_mcp_tools_job.rb +17 -0
- data/app/models/collavre/channel.rb +16 -0
- data/app/models/collavre/comment/approvable.rb +16 -0
- data/app/models/collavre/comment/broadcastable.rb +46 -7
- data/app/models/collavre/comment/notifiable.rb +15 -5
- data/app/models/collavre/comment.rb +87 -32
- data/app/models/collavre/comment_version.rb +1 -1
- data/app/models/collavre/creative/describable.rb +104 -19
- data/app/models/collavre/creative/linkable.rb +9 -0
- data/app/models/collavre/creative/permissible.rb +97 -49
- data/app/models/collavre/creative.rb +73 -5
- data/app/models/collavre/creative_share.rb +78 -26
- data/app/models/collavre/inbox_item.rb +2 -2
- data/app/models/collavre/task.rb +27 -0
- data/app/models/collavre/user.rb +90 -10
- data/app/models/concerns/collavre/indexed_json_columns.rb +58 -0
- data/app/services/collavre/agent_type_classifier.rb +23 -0
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +4 -0
- data/app/services/collavre/ai_agent/message_builder.rb +1 -1
- data/app/services/collavre/ai_agent/session_provisioner.rb +126 -0
- data/app/services/collavre/ai_agent/task_claim_service.rb +96 -0
- data/app/services/collavre/ai_client.rb +81 -12
- data/app/services/collavre/auto_theme_generator.rb +15 -11
- data/app/services/collavre/creatives/children_index.rb +86 -0
- data/app/services/collavre/creatives/comment_badge_index.rb +73 -0
- data/app/services/collavre/creatives/creative_tree_serializer.rb +122 -0
- data/app/services/collavre/creatives/effective_creative_resolution.rb +37 -0
- data/app/services/collavre/creatives/index_query.rb +85 -16
- data/app/services/collavre/creatives/permission_checker.rb +1 -1
- data/app/services/collavre/creatives/permission_filter.rb +144 -17
- data/app/services/collavre/creatives/tree_builder.rb +107 -78
- data/app/services/collavre/gemini_parent_recommender.rb +1 -1
- data/app/services/collavre/http_client.rb +119 -0
- data/app/services/collavre/inbox_reply_service.rb +5 -0
- data/app/services/collavre/link_preview_fetcher.rb +129 -38
- data/app/services/collavre/markdown_converter.rb +13 -3
- data/app/services/collavre/markdown_importer.rb +30 -6
- data/app/services/collavre/mobile/event_summarizer.rb +40 -0
- data/app/services/collavre/orchestration/agent_context_builder.rb +1 -15
- data/app/services/collavre/orchestration/agent_orchestrator.rb +34 -8
- data/app/services/collavre/orchestration/arbiter.rb +16 -0
- data/app/services/collavre/orchestration/matcher.rb +79 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +4 -3
- data/app/services/collavre/orchestration/stuck_detector.rb +141 -34
- data/app/services/collavre/system_events/context_builder.rb +1 -14
- data/app/services/collavre/tools/creative_batch_service.rb +3 -2
- data/app/services/collavre/tools/creative_create_service.rb +8 -8
- data/app/services/collavre/tools/creative_update_service.rb +23 -8
- data/app/services/collavre/tools/preview_attach_service.rb +5 -7
- data/app/services/collavre/typo_corrector.rb +188 -0
- data/app/views/collavre/comments/_comment.html.erb +9 -4
- data/app/views/collavre/comments/_comments_popup.html.erb +25 -4
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +10 -0
- data/app/views/collavre/creatives/_topic_move_members_modal.html.erb +42 -0
- data/app/views/collavre/creatives/index.html.erb +14 -1
- data/app/views/collavre/creatives/slide_view.html.erb +1 -1
- data/app/views/collavre/landing/show.html.erb +52 -10
- data/app/views/collavre/users/show.html.erb +3 -0
- data/app/views/collavre/users/typo_correction.html.erb +50 -0
- data/app/views/layouts/collavre/slide.html.erb +1 -0
- data/config/locales/comments.en.yml +16 -0
- data/config/locales/comments.ko.yml +16 -0
- data/config/locales/creatives.en.yml +6 -0
- data/config/locales/creatives.ko.yml +5 -0
- data/config/locales/integrations.en.yml +1 -1
- data/config/locales/integrations.ko.yml +1 -1
- data/config/locales/landing.en.yml +22 -10
- data/config/locales/landing.ko.yml +22 -10
- data/config/locales/mobile.en.yml +16 -0
- data/config/locales/mobile.ko.yml +16 -0
- data/config/locales/orchestration.en.yml +1 -0
- data/config/locales/orchestration.ko.yml +1 -0
- data/config/locales/users.en.yml +15 -0
- data/config/locales/users.ko.yml +15 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +13 -4
- data/db/migrate/20260612000000_add_topic_concurrency_defer_to_comments.rb +38 -0
- data/db/migrate/20260617090000_add_typo_correction_settings_to_users.rb +18 -0
- data/db/migrate/20260702000001_enforce_system_settings_key_not_null.rb +9 -0
- data/db/migrate/20260702000002_add_creative_index_and_fk_to_tags.rb +11 -0
- data/db/migrate/20260702000003_add_creative_type_index_to_labels.rb +10 -0
- data/db/migrate/20260702000004_add_creative_created_at_index_to_comments.rb +10 -0
- data/db/migrate/20260702000005_promote_channel_config_index_columns.rb +76 -0
- data/db/migrate/20260713000001_add_comments_count_to_creatives.rb +20 -0
- data/db/migrate/20260713000002_add_comment_versions_count_to_comments.rb +20 -0
- data/db/migrate/20260715000000_change_creatives_description_to_plain_text.rb +16 -0
- data/db/seeds.rb +51 -0
- data/lib/collavre/version.rb +1 -1
- data/lib/generators/collavre/install/install_generator.rb +1 -0
- metadata +96 -2
- data/app/services/collavre/tools/description_normalizable.rb +0 -16
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
|
2
|
-
import { renderCreativeTree, dispatchCreativeTreeUpdated } from '../../creatives/tree_renderer'
|
|
2
|
+
import { renderCreativeTree, appendCreativeNodes, dispatchCreativeTreeUpdated } from '../../creatives/tree_renderer'
|
|
3
3
|
import { parseEmojis } from '../../utils/emoji_parser'
|
|
4
4
|
|
|
5
5
|
const TREE_RETRY_DELAYS_MS = [200, 600]
|
|
@@ -14,6 +14,12 @@ export default class extends Controller {
|
|
|
14
14
|
this.abortController = null
|
|
15
15
|
this.loadingIndicator = null
|
|
16
16
|
this._editing = false
|
|
17
|
+
this._pagination = null
|
|
18
|
+
this._sentinel = null
|
|
19
|
+
this._sentinelObserver = null
|
|
20
|
+
this._loadingMore = false
|
|
21
|
+
this._loadMoreAbort = null
|
|
22
|
+
this._loadMoreIndicator = null
|
|
17
23
|
this.handleResize = this.updateAlignmentOffset.bind(this)
|
|
18
24
|
this.handleTreeUpdated = () => this.queueAlignmentUpdate()
|
|
19
25
|
this._handleEditStart = () => { this._editing = true }
|
|
@@ -60,6 +66,7 @@ export default class extends Controller {
|
|
|
60
66
|
document.removeEventListener('creative-editing:start', this._handleEditStart)
|
|
61
67
|
document.removeEventListener('creative-editing:stop', this._handleEditStop)
|
|
62
68
|
document.removeEventListener('creative-sync:refetch', this._handleSyncRefetch)
|
|
69
|
+
this._teardownPagination()
|
|
63
70
|
if (this._debouncedLoadTimer) clearTimeout(this._debouncedLoadTimer)
|
|
64
71
|
if (this._archiveToggleHandler) {
|
|
65
72
|
document.getElementById('toggle-archived-btn')?.removeEventListener('click', this._archiveToggleHandler)
|
|
@@ -140,6 +147,11 @@ export default class extends Controller {
|
|
|
140
147
|
load() {
|
|
141
148
|
if (!this.hasUrlValue) return
|
|
142
149
|
|
|
150
|
+
// A fresh load replaces the whole list (filter change, archive toggle, sync
|
|
151
|
+
// refetch), so any active load-more session is stale — tear it down before
|
|
152
|
+
// re-rendering page 1.
|
|
153
|
+
this._teardownPagination()
|
|
154
|
+
|
|
143
155
|
if (this.abortController) {
|
|
144
156
|
this.abortController.abort()
|
|
145
157
|
}
|
|
@@ -196,6 +208,135 @@ export default class extends Controller {
|
|
|
196
208
|
this.markContentLoaded()
|
|
197
209
|
dispatchCreativeTreeUpdated(this.element)
|
|
198
210
|
this.queueAlignmentUpdate()
|
|
211
|
+
this._setupPagination(data?.pagination)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --- Load-more (paginated "Chats" feed) -------------------------------------
|
|
215
|
+
// Only engaged when the JSON response carries a `pagination` object (the
|
|
216
|
+
// comment filter). For the tree and search views the payload has no
|
|
217
|
+
// pagination, so none of this runs and behavior is unchanged.
|
|
218
|
+
|
|
219
|
+
_setupPagination(pagination) {
|
|
220
|
+
this._teardownPagination()
|
|
221
|
+
this._pagination = pagination || null
|
|
222
|
+
if (!this._pagination || !this._pagination.has_more) return
|
|
223
|
+
if (typeof IntersectionObserver === 'undefined') return
|
|
224
|
+
this._createSentinel()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_createSentinel() {
|
|
228
|
+
const sentinel = document.createElement('div')
|
|
229
|
+
sentinel.className = 'creative-chats-load-sentinel'
|
|
230
|
+
sentinel.setAttribute('aria-hidden', 'true')
|
|
231
|
+
this.element.appendChild(sentinel)
|
|
232
|
+
this._sentinel = sentinel
|
|
233
|
+
this._sentinelObserver = new IntersectionObserver((entries) => {
|
|
234
|
+
if (entries.some((entry) => entry.isIntersecting)) this._loadMore()
|
|
235
|
+
}, { root: null, rootMargin: '300px' })
|
|
236
|
+
this._sentinelObserver.observe(sentinel)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_loadMore() {
|
|
240
|
+
if (this._loadingMore) return
|
|
241
|
+
const nextPage = this._pagination && this._pagination.next_page
|
|
242
|
+
if (!nextPage) return
|
|
243
|
+
|
|
244
|
+
this._loadingMore = true
|
|
245
|
+
this._showLoadMoreIndicator()
|
|
246
|
+
|
|
247
|
+
// Tie this request to an AbortController so a fresh load() / teardown (filter
|
|
248
|
+
// change, archive toggle, sync refetch, disconnect) cancels an in-flight
|
|
249
|
+
// page fetch. Without this the stale promise could append rows for a
|
|
250
|
+
// different urlValue into the freshly rendered list and resurrect the
|
|
251
|
+
// pagination state via _repositionSentinel().
|
|
252
|
+
if (this._loadMoreAbort) this._loadMoreAbort.abort()
|
|
253
|
+
this._loadMoreAbort = new AbortController()
|
|
254
|
+
const signal = this._loadMoreAbort.signal
|
|
255
|
+
|
|
256
|
+
const url = new URL(this.urlValue, window.location.origin)
|
|
257
|
+
url.searchParams.set('page', String(nextPage))
|
|
258
|
+
|
|
259
|
+
fetch(url.pathname + url.search, { headers: { Accept: 'application/json' }, signal })
|
|
260
|
+
.then((response) => {
|
|
261
|
+
if (!response.ok) throw new Error(`Failed to load more chats: ${response.status}`)
|
|
262
|
+
return response.json()
|
|
263
|
+
})
|
|
264
|
+
.then((data) => {
|
|
265
|
+
if (signal.aborted) return
|
|
266
|
+
this._hideLoadMoreIndicator()
|
|
267
|
+
const nodes = Array.isArray(data?.creatives) ? data.creatives : []
|
|
268
|
+
if (nodes.length > 0) {
|
|
269
|
+
appendCreativeNodes(this.element, nodes)
|
|
270
|
+
dispatchCreativeTreeUpdated(this.element)
|
|
271
|
+
this.queueAlignmentUpdate()
|
|
272
|
+
}
|
|
273
|
+
this._loadingMore = false
|
|
274
|
+
this._pagination = data?.pagination || null
|
|
275
|
+
if (this._pagination && this._pagination.has_more) {
|
|
276
|
+
this._repositionSentinel()
|
|
277
|
+
} else {
|
|
278
|
+
this._teardownPagination()
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
.catch((error) => {
|
|
282
|
+
if (error.name === 'AbortError') return
|
|
283
|
+
console.error(error)
|
|
284
|
+
this._hideLoadMoreIndicator()
|
|
285
|
+
this._loadingMore = false
|
|
286
|
+
// Leave the sentinel in place so a later scroll can retry the page.
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_repositionSentinel() {
|
|
291
|
+
// New rows were appended after the sentinel; move it back to the tail so it
|
|
292
|
+
// keeps trailing the list and can trigger the following page.
|
|
293
|
+
if (this._sentinel && this._sentinel.parentNode === this.element) {
|
|
294
|
+
this.element.appendChild(this._sentinel)
|
|
295
|
+
} else {
|
|
296
|
+
this._createSentinel()
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_teardownPagination() {
|
|
301
|
+
if (this._loadMoreAbort) {
|
|
302
|
+
this._loadMoreAbort.abort()
|
|
303
|
+
this._loadMoreAbort = null
|
|
304
|
+
}
|
|
305
|
+
if (this._sentinelObserver) {
|
|
306
|
+
this._sentinelObserver.disconnect()
|
|
307
|
+
this._sentinelObserver = null
|
|
308
|
+
}
|
|
309
|
+
if (this._sentinel && this._sentinel.parentNode) {
|
|
310
|
+
this._sentinel.remove()
|
|
311
|
+
}
|
|
312
|
+
this._sentinel = null
|
|
313
|
+
this._pagination = null
|
|
314
|
+
this._loadingMore = false
|
|
315
|
+
this._hideLoadMoreIndicator()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
_showLoadMoreIndicator() {
|
|
319
|
+
if (this._loadMoreIndicator) return
|
|
320
|
+
const indicator = document.createElement('div')
|
|
321
|
+
indicator.className = 'creative-tree-loading-placeholder creative-chats-load-more'
|
|
322
|
+
indicator.setAttribute('role', 'status')
|
|
323
|
+
indicator.setAttribute('aria-live', 'polite')
|
|
324
|
+
indicator.innerHTML = `
|
|
325
|
+
<span class="creative-loading-indicator" aria-hidden="true">
|
|
326
|
+
<span class="creative-loading-dot">.</span>
|
|
327
|
+
<span class="creative-loading-dot">.</span>
|
|
328
|
+
<span class="creative-loading-dot">.</span>
|
|
329
|
+
</span>
|
|
330
|
+
`
|
|
331
|
+
this.element.appendChild(indicator)
|
|
332
|
+
this._loadMoreIndicator = indicator
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
_hideLoadMoreIndicator() {
|
|
336
|
+
if (this._loadMoreIndicator && this._loadMoreIndicator.parentNode) {
|
|
337
|
+
this._loadMoreIndicator.remove()
|
|
338
|
+
}
|
|
339
|
+
this._loadMoreIndicator = null
|
|
199
340
|
}
|
|
200
341
|
|
|
201
342
|
showEmptyState() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { confirmDialog } from "../lib/utils/dialog"
|
|
2
3
|
|
|
3
4
|
// Connects to data-controller="image-lightbox"
|
|
4
5
|
// Provides a fullscreen image carousel with navigation, download, and zoom
|
|
@@ -338,7 +339,7 @@ export default class extends Controller {
|
|
|
338
339
|
|
|
339
340
|
async _deleteCurrentImage() {
|
|
340
341
|
if (!this.hasDownloadAllUrlValue) return
|
|
341
|
-
if (!
|
|
342
|
+
if (!(await confirmDialog(this.i18nDeleteConfirmValue, { danger: true }))) return
|
|
342
343
|
|
|
343
344
|
const url = `${this.downloadAllUrlValue.replace("download_images", "remove_image")}?index=${this._currentIndex}`
|
|
344
345
|
const csrfToken = document.querySelector("meta[name='csrf-token']")?.content
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
|
2
|
+
import { Turbo } from '@hotwired/turbo-rails'
|
|
3
|
+
import { createSubscription } from '../services/cable'
|
|
4
|
+
|
|
5
|
+
// Keeps the global inbox badge live across WebSocket gaps.
|
|
6
|
+
//
|
|
7
|
+
// Steady-state, the badge is updated by Turbo Stream broadcasts on the
|
|
8
|
+
// `["inbox", user]` stream (see Comment::Broadcastable). Those broadcasts are
|
|
9
|
+
// fire-and-forget: ActionCable never replays a message missed while the socket
|
|
10
|
+
// was down, so a badge update sent during a Turbo navigation gap, sleep/wake,
|
|
11
|
+
// network blip, or server restart was lost until the next full page render
|
|
12
|
+
// (the "badge only appears after refresh" bug).
|
|
13
|
+
//
|
|
14
|
+
// Subscribing to InboxBadgeChannel closes that gap: ActionCable re-runs the
|
|
15
|
+
// channel's #subscribed on every (re)connect, and the server transmits the
|
|
16
|
+
// authoritative badge snapshot straight back over THIS subscription. We render
|
|
17
|
+
// it ourselves (rather than relying on the sibling turbo_stream_from stream,
|
|
18
|
+
// which may not have re-attached yet) so the catch-up can't race a reconnect.
|
|
19
|
+
export default class extends Controller {
|
|
20
|
+
connect() {
|
|
21
|
+
this.subscription = createSubscription(
|
|
22
|
+
{ channel: 'Collavre::InboxBadgeChannel' },
|
|
23
|
+
{ received: (snapshot) => Turbo.renderStreamMessage(snapshot) },
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
disconnect() {
|
|
28
|
+
if (this.subscription) {
|
|
29
|
+
this.subscription.unsubscribe()
|
|
30
|
+
this.subscription = null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -22,6 +22,7 @@ import ClickTargetController from "./click_target_controller"
|
|
|
22
22
|
import TabsController from "./tabs_controller"
|
|
23
23
|
import LinkCreativeController from "./link_creative_controller"
|
|
24
24
|
import TopicSearchController from "./topic_search_controller"
|
|
25
|
+
import TopicListController from "./topic_list_controller"
|
|
25
26
|
import CommonPopupController from "./common_popup_controller"
|
|
26
27
|
import CommentController from "./comment_controller"
|
|
27
28
|
import ReactionPickerController from "./reaction_picker_controller"
|
|
@@ -34,6 +35,7 @@ import ShareModalController from "./share_modal_controller"
|
|
|
34
35
|
import ImageLightboxController from "./image_lightbox_controller"
|
|
35
36
|
import SearchPopupController from "./search_popup_controller"
|
|
36
37
|
import LandingVideoController from "./landing_video_controller"
|
|
38
|
+
import InboxBadgeController from "./inbox_badge_controller"
|
|
37
39
|
|
|
38
40
|
// Export all controllers
|
|
39
41
|
export {
|
|
@@ -58,6 +60,7 @@ export {
|
|
|
58
60
|
TabsController,
|
|
59
61
|
LinkCreativeController,
|
|
60
62
|
TopicSearchController,
|
|
63
|
+
TopicListController,
|
|
61
64
|
CommonPopupController,
|
|
62
65
|
CommentController,
|
|
63
66
|
ReactionPickerController,
|
|
@@ -69,7 +72,8 @@ export {
|
|
|
69
72
|
ImageLightboxController,
|
|
70
73
|
SearchPopupController,
|
|
71
74
|
CommentBadgeController,
|
|
72
|
-
LandingVideoController
|
|
75
|
+
LandingVideoController,
|
|
76
|
+
InboxBadgeController
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
// Registration function for use with a Stimulus application
|
|
@@ -96,6 +100,7 @@ export function registerControllers(application) {
|
|
|
96
100
|
application.register("tabs", TabsController)
|
|
97
101
|
application.register("link-creative", LinkCreativeController)
|
|
98
102
|
application.register("topic-search", TopicSearchController)
|
|
103
|
+
application.register("topic-list", TopicListController)
|
|
99
104
|
application.register("common-popup", CommonPopupController)
|
|
100
105
|
application.register("comment", CommentController)
|
|
101
106
|
application.register("reaction-picker", ReactionPickerController)
|
|
@@ -108,4 +113,5 @@ export function registerControllers(application) {
|
|
|
108
113
|
application.register("search-popup", SearchPopupController)
|
|
109
114
|
application.register("comment-badge", CommentBadgeController)
|
|
110
115
|
application.register("landing-video", LandingVideoController)
|
|
116
|
+
application.register("inbox-badge", InboxBadgeController)
|
|
111
117
|
}
|
|
@@ -9,9 +9,14 @@ export default class extends Controller {
|
|
|
9
9
|
}
|
|
10
10
|
document.addEventListener('keydown', this._escHandler)
|
|
11
11
|
|
|
12
|
-
// Global keyboard shortcut: Cmd/Ctrl + K to open search
|
|
12
|
+
// Global keyboard shortcut: Cmd/Ctrl + K to open search.
|
|
13
|
+
// Skip when focus is inside an editable element (input, textarea,
|
|
14
|
+
// contenteditable, or the inline Lexical editor) so that element's own
|
|
15
|
+
// Ctrl+K behavior (e.g. delete-to-end-of-line) takes precedence over the
|
|
16
|
+
// global search popup.
|
|
13
17
|
this._shortcutHandler = (e) => {
|
|
14
18
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
19
|
+
if (this._isEditableTarget(e.target)) return
|
|
15
20
|
e.preventDefault()
|
|
16
21
|
this.toggle()
|
|
17
22
|
}
|
|
@@ -19,6 +24,17 @@ export default class extends Controller {
|
|
|
19
24
|
document.addEventListener('keydown', this._shortcutHandler)
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
_isEditableTarget(target) {
|
|
28
|
+
const el = target instanceof Element ? target : document.activeElement
|
|
29
|
+
if (!el || typeof el.closest !== 'function') return false
|
|
30
|
+
const tag = el.tagName
|
|
31
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true
|
|
32
|
+
if (el.isContentEditable) return true
|
|
33
|
+
return Boolean(
|
|
34
|
+
el.closest('[contenteditable]:not([contenteditable="false"]), [data-lexical-editor-root]')
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
disconnect() {
|
|
23
39
|
document.removeEventListener('keydown', this._escHandler)
|
|
24
40
|
document.removeEventListener('keydown', this._shortcutHandler)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { confirmDialog, alertDialog } from "../lib/utils/dialog"
|
|
2
3
|
|
|
3
4
|
export default class extends Controller {
|
|
4
5
|
static targets = ["container"]
|
|
@@ -277,13 +278,13 @@ export default class extends Controller {
|
|
|
277
278
|
const methodInput = form.querySelector("input[name='_method'][value='delete']")
|
|
278
279
|
if (!methodInput) return
|
|
279
280
|
|
|
280
|
-
form.addEventListener("submit", (e) => {
|
|
281
|
+
form.addEventListener("submit", async (e) => {
|
|
281
282
|
e.preventDefault()
|
|
282
283
|
|
|
283
284
|
const confirmMessage = form.dataset.turboConfirm
|
|
284
285
|
|| form.querySelector("button[type='submit']")?.dataset?.turboConfirm
|
|
285
286
|
|| form.querySelector("button")?.dataset?.confirm
|
|
286
|
-
if (confirmMessage && !
|
|
287
|
+
if (confirmMessage && !(await confirmDialog(confirmMessage, { danger: true }))) return
|
|
287
288
|
|
|
288
289
|
const listItem = form.closest("li")
|
|
289
290
|
if (listItem) {
|
|
@@ -379,7 +380,7 @@ export default class extends Controller {
|
|
|
379
380
|
const copiedTemplate = inviteLinkBtn.dataset.copiedTemplate
|
|
380
381
|
|
|
381
382
|
if (permission === "no_access") {
|
|
382
|
-
|
|
383
|
+
alertDialog(noAccessMessage)
|
|
383
384
|
return
|
|
384
385
|
}
|
|
385
386
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import CommonPopupController from './common_popup_controller'
|
|
2
|
+
|
|
3
|
+
const ICON_ARCHIVE = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></svg>`
|
|
4
|
+
|
|
5
|
+
export default class extends CommonPopupController {
|
|
6
|
+
static targets = ['input', 'list', 'close']
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
super.connect()
|
|
10
|
+
this._allItems = []
|
|
11
|
+
this._onInputBound = this._onInput.bind(this)
|
|
12
|
+
this._onKeydownBound = this.handleInputKeydown.bind(this)
|
|
13
|
+
this._onCloseBound = () => this.close()
|
|
14
|
+
this.inputTarget.addEventListener('input', this._onInputBound)
|
|
15
|
+
this.inputTarget.addEventListener('keydown', this._onKeydownBound)
|
|
16
|
+
this.closeTarget.addEventListener('click', this._onCloseBound)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
disconnect() {
|
|
20
|
+
this.inputTarget?.removeEventListener('input', this._onInputBound)
|
|
21
|
+
this.inputTarget?.removeEventListener('keydown', this._onKeydownBound)
|
|
22
|
+
this.closeTarget?.removeEventListener('click', this._onCloseBound)
|
|
23
|
+
super.disconnect()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
openForTopics({ topics = [], archivedTopics = [], mainTopicId = null, allMessagesLabel = 'All Messages' }, anchorRect, onSelectCallback, boundsElement = null) {
|
|
27
|
+
this.onSelectCallback = onSelectCallback
|
|
28
|
+
this._allItems = this._buildItems({ topics, archivedTopics, mainTopicId, allMessagesLabel })
|
|
29
|
+
this.inputTarget.value = ''
|
|
30
|
+
this.setItems(this._allItems)
|
|
31
|
+
super.open(anchorRect, boundsElement)
|
|
32
|
+
requestAnimationFrame(() => this.inputTarget.focus())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_buildItems({ topics, archivedTopics, mainTopicId, allMessagesLabel }) {
|
|
36
|
+
const main = mainTopicId ? topics.find(t => String(t.id) === String(mainTopicId)) : null
|
|
37
|
+
const others = topics.filter(t => !main || String(t.id) !== String(mainTopicId))
|
|
38
|
+
const items = []
|
|
39
|
+
if (main) items.push({ id: main.id, label: `#${main.name}`, archived: false })
|
|
40
|
+
others.forEach(t => items.push({ id: t.id, label: `#${t.name}`, archived: false }))
|
|
41
|
+
items.push({ id: '', label: `📋 ${allMessagesLabel}`, archived: false })
|
|
42
|
+
archivedTopics.forEach(t => items.push({ id: t.id, label: `#${t.name}`, archived: true }))
|
|
43
|
+
return items
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_onInput() {
|
|
47
|
+
const q = this.inputTarget.value.toLowerCase().trim()
|
|
48
|
+
if (!q) { this.setItems(this._allItems); return }
|
|
49
|
+
const filtered = this._allItems.filter(i => (i.label || '').toLowerCase().includes(q))
|
|
50
|
+
this.setItems(filtered)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
handleInputKeydown(event) {
|
|
54
|
+
if (this.handleKey(event)) return
|
|
55
|
+
if (event.key === 'Escape') this.close()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
select(item) {
|
|
59
|
+
if (this.onSelectCallback) this.onSelectCallback(item)
|
|
60
|
+
this.close()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
renderItem(item) {
|
|
64
|
+
const escaped = String(item.label || '')
|
|
65
|
+
.replace(/&/g, '&')
|
|
66
|
+
.replace(/</g, '<')
|
|
67
|
+
.replace(/>/g, '>')
|
|
68
|
+
if (item.archived) {
|
|
69
|
+
return `<span class="topic-list-item topic-list-item--archived">${ICON_ARCHIVE} ${escaped}</span>`
|
|
70
|
+
}
|
|
71
|
+
return `<span class="topic-list-item">${escaped}</span>`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
dispatchClose(reason) {
|
|
75
|
+
this.onSelectCallback = null
|
|
76
|
+
super.dispatchClose(reason)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import CommonPopupController from './common_popup_controller'
|
|
2
2
|
import { fetchNextTopicName, createTopicWithComments } from '../lib/api/topics'
|
|
3
|
+
import { alertDialog } from '../lib/utils/dialog'
|
|
3
4
|
|
|
4
5
|
export default class extends CommonPopupController {
|
|
5
6
|
static targets = ['input', 'list', 'close']
|
|
@@ -148,7 +149,7 @@ export default class extends CommonPopupController {
|
|
|
148
149
|
this.onSelectCallback({ id: result.topic.id, label: `#${result.topic.name}`, created: true })
|
|
149
150
|
}
|
|
150
151
|
} else {
|
|
151
|
-
|
|
152
|
+
alertDialog(result.error)
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
import { createMoveContext, applyMove, revertMove } from './operations';
|
|
27
27
|
import { sendNewOrder, sendLinkedCreative, sendTopicMove } from '../../lib/api/drag_drop';
|
|
28
28
|
import { initIndicator, showLinkHover, hideLinkHover } from './indicator';
|
|
29
|
+
import { showMissingMembersPopup } from '../topic_move_members_popup';
|
|
30
|
+
import { alertDialog } from '../../lib/utils/dialog';
|
|
29
31
|
|
|
30
32
|
const childZoneRatio = 0.3;
|
|
31
33
|
const coordPrecision = 5;
|
|
@@ -500,9 +502,7 @@ function getDraggedContext(event) {
|
|
|
500
502
|
|
|
501
503
|
function notifyInvalidDrop() {
|
|
502
504
|
console.error('Rejected invalid creative drop payload');
|
|
503
|
-
|
|
504
|
-
window.alert(INVALID_DROP_MESSAGE);
|
|
505
|
-
}
|
|
505
|
+
alertDialog(INVALID_DROP_MESSAGE);
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
import { attachBundleDragImage } from '../../utils/drag_bundle_image.js';
|
|
@@ -640,15 +640,24 @@ export function handleDrop(event) {
|
|
|
640
640
|
if (sourceCreativeId === targetCreativeId) return;
|
|
641
641
|
|
|
642
642
|
sendTopicMove({ topicId, sourceCreativeId, targetCreativeId })
|
|
643
|
-
.then(() => {
|
|
643
|
+
.then((data) => {
|
|
644
644
|
// Dispatch event so topic list refreshes
|
|
645
645
|
window.dispatchEvent(new CustomEvent('collavre:topic-moved', {
|
|
646
646
|
detail: { topicId, sourceCreativeId, targetCreativeId }
|
|
647
647
|
}));
|
|
648
|
+
|
|
649
|
+
// Offer to re-add members who lose access at the new location.
|
|
650
|
+
if (data && Array.isArray(data.missing_members) && data.missing_members.length > 0) {
|
|
651
|
+
showMissingMembersPopup({
|
|
652
|
+
members: data.missing_members,
|
|
653
|
+
targetCreativeId: data.target_creative_id || targetCreativeId,
|
|
654
|
+
targetCreativeName: data.target_creative_name,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
648
657
|
})
|
|
649
658
|
.catch((error) => {
|
|
650
659
|
console.error('Failed to move topic', error);
|
|
651
|
-
|
|
660
|
+
alertDialog(error.message || 'Failed to move topic');
|
|
652
661
|
});
|
|
653
662
|
} catch (error) {
|
|
654
663
|
console.error('Failed to parse topic move data', error);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import csrfFetch from '../lib/api/csrf_fetch';
|
|
2
|
+
|
|
3
|
+
// After a topic is moved to another creative, TopicsController#move returns the
|
|
4
|
+
// members who had access on the source creative but not on the target. This
|
|
5
|
+
// module renders the (hidden) `#topic-move-members-modal` partial with those
|
|
6
|
+
// members and lets the user re-add them with one click. All user-facing text
|
|
7
|
+
// comes from the partial's i18n data-attributes — nothing is hardcoded here.
|
|
8
|
+
|
|
9
|
+
function getModal() {
|
|
10
|
+
return document.getElementById('topic-move-members-modal');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function closeModal(modal) {
|
|
14
|
+
modal.style.display = 'none';
|
|
15
|
+
document.body.classList.remove('no-scroll');
|
|
16
|
+
const list = modal.querySelector('#topic-move-members-list');
|
|
17
|
+
if (list) list.innerHTML = '';
|
|
18
|
+
const message = modal.querySelector('#topic-move-members-message');
|
|
19
|
+
if (message) message.innerHTML = '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildRow(modal, member) {
|
|
23
|
+
const template = modal.querySelector('#topic-move-member-row');
|
|
24
|
+
const row = template.content.firstElementChild.cloneNode(true);
|
|
25
|
+
const user = member.user || {};
|
|
26
|
+
|
|
27
|
+
const avatar = row.querySelector('.tmm-avatar');
|
|
28
|
+
if (user.avatar_url && !user.default_avatar) {
|
|
29
|
+
const img = document.createElement('img');
|
|
30
|
+
img.src = user.avatar_url;
|
|
31
|
+
img.alt = '';
|
|
32
|
+
img.style.width = '100%';
|
|
33
|
+
img.style.height = '100%';
|
|
34
|
+
img.style.objectFit = 'cover';
|
|
35
|
+
avatar.appendChild(img);
|
|
36
|
+
} else {
|
|
37
|
+
avatar.textContent = user.initial || '?';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
row.querySelector('.tmm-name').textContent = user.name || user.email || '';
|
|
41
|
+
row.querySelector('.tmm-email').textContent = user.email || '';
|
|
42
|
+
|
|
43
|
+
const select = row.querySelector('.tmm-permission');
|
|
44
|
+
if (member.permission) {
|
|
45
|
+
const option = Array.from(select.options).find((o) => o.value === member.permission);
|
|
46
|
+
if (option) select.value = member.permission;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
row.dataset.userEmail = user.email || '';
|
|
50
|
+
return row;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function setMessage(modal, text) {
|
|
54
|
+
const message = modal.querySelector('#topic-move-members-message');
|
|
55
|
+
if (message) message.textContent = text || '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function addSelected(modal, targetCreativeId) {
|
|
59
|
+
const addBtn = modal.querySelector('#topic-move-members-add');
|
|
60
|
+
const rows = Array.from(modal.querySelectorAll('.topic-move-member-row')).filter((row) => {
|
|
61
|
+
const checkbox = row.querySelector('.tmm-checkbox');
|
|
62
|
+
return checkbox && checkbox.checked && row.dataset.userEmail;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (rows.length === 0) {
|
|
66
|
+
closeModal(modal);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
addBtn.disabled = true;
|
|
71
|
+
setMessage(modal, addBtn.dataset.addingText || modal.dataset.addingText || '');
|
|
72
|
+
|
|
73
|
+
const urlTemplate = modal.dataset.sharesUrlTemplate || modal.dataset.sharesUrl;
|
|
74
|
+
const url = urlTemplate.replace('__CREATIVE_ID__', encodeURIComponent(targetCreativeId));
|
|
75
|
+
|
|
76
|
+
let added = 0;
|
|
77
|
+
let failed = 0;
|
|
78
|
+
|
|
79
|
+
// Sequential so we reuse the existing per-user share endpoint (with all its
|
|
80
|
+
// invitation / linked-creative / contact side effects) and keep ordering.
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
const status = row.querySelector('.tmm-status');
|
|
83
|
+
const select = row.querySelector('.tmm-permission');
|
|
84
|
+
const body = new FormData();
|
|
85
|
+
body.append('user_email', row.dataset.userEmail);
|
|
86
|
+
body.append('permission', select ? select.value : 'feedback');
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// eslint-disable-next-line no-await-in-loop
|
|
90
|
+
const response = await csrfFetch(url, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { Accept: 'application/json' },
|
|
93
|
+
body,
|
|
94
|
+
});
|
|
95
|
+
if (response.ok) {
|
|
96
|
+
added += 1;
|
|
97
|
+
if (status) status.textContent = '✓';
|
|
98
|
+
row.style.opacity = '0.6';
|
|
99
|
+
} else {
|
|
100
|
+
failed += 1;
|
|
101
|
+
if (status) status.textContent = '✗';
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
failed += 1;
|
|
105
|
+
if (status) status.textContent = '✗';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (failed === 0) {
|
|
110
|
+
const template = added === 1
|
|
111
|
+
? (modal.dataset.addedOne || '')
|
|
112
|
+
: (modal.dataset.addedOther || '');
|
|
113
|
+
setMessage(modal, template.replace('__COUNT__', added));
|
|
114
|
+
setTimeout(() => closeModal(modal), 1200);
|
|
115
|
+
} else if (added === 0) {
|
|
116
|
+
setMessage(modal, modal.dataset.failedText || '');
|
|
117
|
+
addBtn.disabled = false;
|
|
118
|
+
} else {
|
|
119
|
+
const template = modal.dataset.partialText || '';
|
|
120
|
+
setMessage(modal, template.replace('__ADDED__', added).replace('__FAILED__', failed));
|
|
121
|
+
addBtn.disabled = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function showMissingMembersPopup({ members, targetCreativeId, targetCreativeName }) {
|
|
126
|
+
if (!Array.isArray(members) || members.length === 0) return;
|
|
127
|
+
const modal = getModal();
|
|
128
|
+
if (!modal) return;
|
|
129
|
+
|
|
130
|
+
const list = modal.querySelector('#topic-move-members-list');
|
|
131
|
+
list.innerHTML = '';
|
|
132
|
+
members.forEach((member) => list.appendChild(buildRow(modal, member)));
|
|
133
|
+
|
|
134
|
+
const description = modal.querySelector('#topic-move-members-description');
|
|
135
|
+
if (description) {
|
|
136
|
+
const template = modal.dataset.descriptionTemplate || '';
|
|
137
|
+
description.textContent = template.replace('__CREATIVE_NAME__', targetCreativeName || '');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setMessage(modal, '');
|
|
141
|
+
const addBtn = modal.querySelector('#topic-move-members-add');
|
|
142
|
+
addBtn.disabled = false;
|
|
143
|
+
|
|
144
|
+
// (Re)wire controls. Using onclick keeps a single handler across re-opens.
|
|
145
|
+
modal.querySelector('#topic-move-members-close').onclick = () => closeModal(modal);
|
|
146
|
+
modal.querySelector('#topic-move-members-skip').onclick = () => closeModal(modal);
|
|
147
|
+
modal.onclick = (event) => {
|
|
148
|
+
if (event.target === modal) closeModal(modal);
|
|
149
|
+
};
|
|
150
|
+
addBtn.onclick = () => addSelected(modal, targetCreativeId);
|
|
151
|
+
|
|
152
|
+
modal.style.display = 'flex';
|
|
153
|
+
modal.style.alignItems = 'center';
|
|
154
|
+
modal.style.justifyContent = 'center';
|
|
155
|
+
document.body.classList.add('no-scroll');
|
|
156
|
+
}
|
|
@@ -169,6 +169,9 @@ function applyRowProperties(row, node) {
|
|
|
169
169
|
if (Object.prototype.hasOwnProperty.call(inlinePayload, 'markdown_source')) {
|
|
170
170
|
setDatasetValue(row, 'markdownSource', inlinePayload.markdown_source ?? '')
|
|
171
171
|
}
|
|
172
|
+
if (Object.prototype.hasOwnProperty.call(inlinePayload, 'markdown_editor')) {
|
|
173
|
+
setDatasetValue(row, 'markdownEditor', inlinePayload.markdown_editor ?? '')
|
|
174
|
+
}
|
|
172
175
|
|
|
173
176
|
if (dirty && typeof row.requestUpdate === 'function') {
|
|
174
177
|
// Before Lit re-renders, sync progressHtml from current DOM.
|
|
@@ -308,6 +311,14 @@ export function renderCreativeTree(container, nodes, { replace = true } = {}) {
|
|
|
308
311
|
reconcileNodes(container, nodes)
|
|
309
312
|
}
|
|
310
313
|
|
|
314
|
+
// Append a page of flat nodes to an already-rendered list without clearing the
|
|
315
|
+
// existing rows. Used by the "Chats" feed's load-more, where each page is added
|
|
316
|
+
// below the previous one rather than replacing it.
|
|
317
|
+
export function appendCreativeNodes(container, nodes) {
|
|
318
|
+
if (!container) return
|
|
319
|
+
appendNodes(container, nodes)
|
|
320
|
+
}
|
|
321
|
+
|
|
311
322
|
export function dispatchCreativeTreeUpdated(container) {
|
|
312
323
|
if (!container) return
|
|
313
324
|
const event = new CustomEvent('creative-tree:updated', { bubbles: true })
|