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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/comment_versions.css +76 -0
  3. data/app/assets/stylesheets/collavre/comments_popup.css +347 -37
  4. data/app/assets/stylesheets/collavre/creatives.css +73 -1
  5. data/app/assets/stylesheets/collavre/org_chart.css +319 -0
  6. data/app/assets/stylesheets/collavre/popup.css +68 -1
  7. data/app/controllers/collavre/application_controller.rb +13 -0
  8. data/app/controllers/collavre/comments/versions_controller.rb +82 -0
  9. data/app/controllers/collavre/comments_controller.rb +14 -153
  10. data/app/controllers/collavre/concerns/exportable.rb +30 -0
  11. data/app/controllers/collavre/concerns/shareable.rb +28 -0
  12. data/app/controllers/collavre/concerns/slide_viewable.rb +37 -0
  13. data/app/controllers/collavre/concerns/tree_manageable.rb +141 -0
  14. data/app/controllers/collavre/creative_imports_controller.rb +6 -0
  15. data/app/controllers/collavre/creative_invitations_controller.rb +46 -0
  16. data/app/controllers/collavre/creative_plans_controller.rb +1 -1
  17. data/app/controllers/collavre/creative_shares_controller.rb +84 -14
  18. data/app/controllers/collavre/creatives_controller.rb +70 -194
  19. data/app/controllers/collavre/google_auth_controller.rb +3 -0
  20. data/app/controllers/collavre/invites_controller.rb +2 -1
  21. data/app/controllers/collavre/sessions_controller.rb +3 -0
  22. data/app/controllers/collavre/topics_controller.rb +39 -2
  23. data/app/controllers/collavre/users_controller.rb +5 -404
  24. data/app/controllers/concerns/collavre/comments/approval_actions.rb +108 -0
  25. data/app/controllers/concerns/collavre/comments/batch_operations.rb +55 -0
  26. data/app/controllers/concerns/collavre/comments/conversion.rb +46 -0
  27. data/app/controllers/concerns/collavre/users_controller/admin_operations.rb +74 -0
  28. data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +119 -0
  29. data/app/controllers/concerns/collavre/users_controller/contact_management.rb +166 -0
  30. data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +102 -0
  31. data/app/controllers/concerns/collavre/users_controller/registration.rb +63 -0
  32. data/app/helpers/collavre/application_helper.rb +1 -0
  33. data/app/helpers/collavre/creatives_helper.rb +12 -9
  34. data/app/helpers/collavre/navigation_helper.rb +1 -1
  35. data/app/javascript/collavre.js +0 -1
  36. data/app/javascript/controllers/comment_controller.js +33 -70
  37. data/app/javascript/controllers/comment_version_controller.js +164 -0
  38. data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +305 -0
  39. data/app/javascript/controllers/comments/__tests__/list_controller_selection.test.js +103 -0
  40. data/app/javascript/controllers/comments/__tests__/review_quotes_store.test.js +113 -0
  41. data/app/javascript/controllers/comments/contexts_controller.js +363 -0
  42. data/app/javascript/controllers/comments/form_controller.js +304 -13
  43. data/app/javascript/controllers/comments/list_controller.js +151 -62
  44. data/app/javascript/controllers/comments/popup_controller.js +66 -38
  45. data/app/javascript/controllers/comments/presence_controller.js +2 -10
  46. data/app/javascript/controllers/comments/review_quotes_store.js +189 -0
  47. data/app/javascript/controllers/comments/topics_controller.js +34 -10
  48. data/app/javascript/controllers/index.js +15 -1
  49. data/app/javascript/controllers/org_chart_controller.js +46 -0
  50. data/app/javascript/controllers/share_modal_controller.js +369 -0
  51. data/app/javascript/controllers/topic_search_controller.js +103 -0
  52. data/app/javascript/creatives/drag_drop/event_handlers.js +42 -1
  53. data/app/javascript/lib/api/creatives.js +12 -0
  54. data/app/javascript/lib/api/csrf_fetch.js +35 -0
  55. data/app/javascript/lib/api/drag_drop.js +17 -0
  56. data/app/javascript/modules/command_menu.js +40 -0
  57. data/app/javascript/modules/creative_row_editor.js +88 -0
  58. data/app/javascript/modules/slide_view.js +2 -1
  59. data/app/jobs/collavre/ai_agent_job.rb +42 -30
  60. data/app/jobs/collavre/compress_job.rb +92 -0
  61. data/app/models/collavre/comment.rb +36 -1
  62. data/app/models/collavre/comment_version.rb +15 -0
  63. data/app/models/collavre/creative/describable.rb +1 -1
  64. data/app/models/collavre/creative.rb +51 -0
  65. data/app/models/collavre/task.rb +30 -2
  66. data/app/models/collavre/user.rb +20 -3
  67. data/app/services/collavre/ai_agent/a2a_dispatcher.rb +68 -0
  68. data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +89 -0
  69. data/app/services/collavre/ai_agent/message_builder.rb +85 -6
  70. data/app/services/collavre/ai_agent/response_finalizer.rb +97 -0
  71. data/app/services/collavre/ai_agent/response_streamer.rb +56 -0
  72. data/app/services/collavre/ai_agent/review_handler.rb +18 -1
  73. data/app/services/collavre/ai_agent_service.rb +130 -183
  74. data/app/services/collavre/ai_client.rb +6 -0
  75. data/app/services/collavre/auto_theme_generator.rb +1 -1
  76. data/app/services/collavre/command_menu_service.rb +19 -0
  77. data/app/services/collavre/comments/command_processor.rb +3 -1
  78. data/app/services/collavre/comments/compress_command.rb +75 -0
  79. data/app/services/collavre/comments/concerns/workflow_support.rb +115 -0
  80. data/app/services/collavre/comments/work_command.rb +161 -0
  81. data/app/services/collavre/comments/workflow_executor.rb +276 -0
  82. data/app/services/collavre/creatives/plan_tagger.rb +14 -3
  83. data/app/services/collavre/creatives/tree_formatter.rb +53 -13
  84. data/app/services/collavre/gemini_parent_recommender.rb +4 -4
  85. data/app/services/collavre/orchestration/agent_context_builder.rb +1 -3
  86. data/app/services/collavre/orchestration/agent_orchestrator.rb +15 -4
  87. data/app/services/collavre/orchestration/policy_resolver.rb +0 -19
  88. data/app/services/collavre/orchestration/scheduler.rb +3 -2
  89. data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
  90. data/app/services/collavre/system_events/dispatcher.rb +9 -0
  91. data/app/services/collavre/tools/creative_create_service.rb +1 -8
  92. data/app/services/collavre/tools/creative_import_service.rb +46 -0
  93. data/app/services/collavre/tools/creative_retrieval_service.rb +157 -96
  94. data/app/services/collavre/tools/creative_update_service.rb +1 -8
  95. data/app/services/collavre/tools/cron_list_service.rb +1 -1
  96. data/app/services/collavre/tools/description_normalizable.rb +16 -0
  97. data/app/views/collavre/comments/_comment.html.erb +25 -8
  98. data/app/views/collavre/comments/_comments_popup.html.erb +32 -5
  99. data/app/views/collavre/creatives/_inline_edit_form.html.erb +13 -0
  100. data/app/views/collavre/creatives/_share_button.html.erb +4 -1
  101. data/app/views/collavre/creatives/_share_modal.html.erb +31 -1
  102. data/app/views/collavre/creatives/index.html.erb +5 -5
  103. data/app/views/collavre/creatives/slide_view.html.erb +1 -1
  104. data/app/views/collavre/users/{_contact_management.html.erb → _contact_list.html.erb} +4 -8
  105. data/app/views/collavre/users/_org_chart.html.erb +68 -0
  106. data/app/views/collavre/users/_org_chart_node.html.erb +169 -0
  107. data/app/views/collavre/users/new_ai.html.erb +9 -0
  108. data/app/views/collavre/users/show.html.erb +32 -8
  109. data/config/locales/comments.en.yml +57 -2
  110. data/config/locales/comments.ko.yml +57 -2
  111. data/config/locales/contacts.en.yml +31 -0
  112. data/config/locales/contacts.ko.yml +31 -0
  113. data/config/locales/contexts.en.yml +8 -0
  114. data/config/locales/contexts.ko.yml +8 -0
  115. data/config/locales/creatives.en.yml +6 -0
  116. data/config/locales/creatives.ko.yml +6 -0
  117. data/config/locales/users.en.yml +1 -0
  118. data/config/locales/users.ko.yml +1 -0
  119. data/config/routes.rb +14 -1
  120. data/db/migrate/20260220072200_add_workflow_fields_to_tasks.rb +12 -0
  121. data/db/migrate/20260223173533_add_review_type_to_comments.rb +5 -0
  122. data/db/migrate/20260225065200_create_comment_versions.rb +14 -0
  123. data/db/migrate/20260225074416_add_selected_version_id_to_comments.rb +7 -0
  124. data/lib/collavre/version.rb +1 -1
  125. metadata +47 -10
  126. data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +0 -91
  127. data/app/javascript/lib/lexical/action_text_attachment_node.js +0 -459
  128. data/app/javascript/lib/lexical/dom_attachment_utils.js +0 -66
  129. data/app/javascript/modules/share_modal.js +0 -76
  130. data/app/javascript/modules/share_user_popup.js +0 -77
  131. data/app/services/collavre/orchestration/self_reflection_evaluator.rb +0 -231
  132. data/app/views/collavre/comments/_presence_avatars.html.erb +0 -8
  133. data/app/views/collavre/creatives/_delete_button.html.erb +0 -12
@@ -1,10 +1,15 @@
1
1
  module Collavre
2
2
  class CreativesController < ApplicationController
3
+ include Collavre::Concerns::SlideViewable
4
+ include Collavre::Concerns::Exportable
5
+ include Collavre::Concerns::TreeManageable
6
+ include Collavre::Concerns::Shareable
7
+
3
8
  # TODO: for not for security reasons for this Collavre app, we don't expose to public, later it should be controlled by roles for each Creatives
4
9
  # Removed unauthenticated access to index and show actions
5
10
  allow_unauthenticated_access only: %i[ index children export_markdown show slide_view ]
6
11
  before_action :enforce_creatives_login_policy, only: %i[ index children export_markdown show slide_view ]
7
- before_action :set_creative, only: %i[ show edit update destroy request_permission parent_suggestions slide_view unconvert ]
12
+ before_action :set_creative, only: %i[ show edit update destroy parent_suggestions slide_view request_permission unconvert contexts update_contexts update_metadata ]
8
13
 
9
14
  def index
10
15
  respond_to do |format|
@@ -138,29 +143,14 @@ module Collavre
138
143
  progress_html: view_context.render_creative_progress(@creative),
139
144
  depth: depth,
140
145
  prompt: @creative.prompt_for(Current.user),
141
- has_children: children_count > 0
146
+ has_children: children_count > 0,
147
+ data: @creative.effective_origin(Set.new).data
142
148
  }
143
149
  end
144
150
  end
145
151
  end
146
152
  end
147
153
 
148
- def slide_view
149
- unless @creative.has_permission?(Current.user, :read)
150
- if Current.user
151
- redirect_to creatives_path, alert: t("collavre.creatives.errors.no_permission")
152
- else
153
- request_authentication
154
- end
155
- return
156
- end
157
-
158
- @slide_ids = []
159
- @root_depth = @creative.ancestors.count
160
- build_slide_ids(@creative)
161
- render layout: "collavre/slide"
162
- end
163
-
164
154
  def new
165
155
  @creative = Creative.new
166
156
  if params[:parent_id].present?
@@ -244,173 +234,92 @@ module Collavre
244
234
  end
245
235
  end
246
236
 
247
- def destroy
248
- parent = @creative.parent
249
- unless @creative.has_permission?(Current.user, :admin)
250
- redirect_to @creative, alert: t("collavre.creatives.errors.no_permission") and return
251
- end
252
- Creatives::DestroyService.new(
253
- creative: @creative,
254
- user: Current.user,
255
- delete_with_children: params[:delete_with_children].present?
256
- ).call
257
- end
258
-
259
- def request_permission
260
- creative = @creative.effective_origin
261
- if creative.user == Current.user || creative.has_permission?(Current.user, :read)
262
- return head :unprocessable_entity
263
- end
237
+ def contexts
238
+ creative = @creative.effective_origin(Set.new)
239
+ own_ids = creative.context_ids - [ creative.id ]
240
+ inherited_ids = (creative.effective_context_ids - own_ids - [ creative.id ]).uniq
241
+ own_creatives = Creative.where(id: own_ids).index_by(&:id)
242
+ inherited_creatives = Creative.where(id: inherited_ids).index_by(&:id)
264
243
 
265
- short_title = helpers.strip_tags(creative.effective_origin.description).truncate(10)
266
-
267
- InboxItem.create!(
268
- owner: creative.user,
269
- message_key: "inbox.permission_requested",
270
- message_params: { user: Current.user.display_name, short_title: short_title },
271
- link: creative_url(
272
- creative,
273
- Rails.application.config.action_mailer.default_url_options.merge(share_request: Current.user.email)
274
- )
275
- )
276
-
277
- head :ok
278
- end
244
+ disabled_ids = Array(creative.data&.dig("disabled_context_ids"))
279
245
 
246
+ own = own_ids.filter_map do |cid|
247
+ c = own_creatives[cid]
248
+ next unless c
280
249
 
250
+ { id: c.id, description: c.creative_snippet, inherited: false, disabled: disabled_ids.include?(cid) }
251
+ end
281
252
 
282
- def reorder
283
- dragged_ids = Array(params[:dragged_ids]).map(&:presence).compact
284
- target_id = params[:target_id]
285
- direction = params[:direction]
253
+ inherited = inherited_ids.filter_map do |cid|
254
+ c = inherited_creatives[cid]
255
+ next unless c
286
256
 
287
- if dragged_ids.any?
288
- reorderer.reorder_multiple(
289
- dragged_ids: dragged_ids,
290
- target_id: target_id,
291
- direction: direction
292
- )
293
- else
294
- reorderer.reorder(
295
- dragged_id: params[:dragged_id],
296
- target_id: target_id,
297
- direction: direction
298
- )
257
+ { id: c.id, description: c.creative_snippet, inherited: true, disabled: disabled_ids.include?(cid) }
299
258
  end
300
- head :ok
301
- rescue ::Creatives::Reorderer::Error
302
- head :unprocessable_entity
303
- end
304
-
305
- def link_drop
306
- result = reorderer.link_drop(
307
- dragged_id: params[:dragged_id],
308
- target_id: params[:target_id],
309
- direction: params[:direction]
310
- )
311
-
312
- new_creative = result.new_creative
313
- level = new_creative.ancestors.count + 1
314
- nodes = build_tree(
315
- [ new_creative ],
316
- params: params,
317
- expanded_state_map: {},
318
- level: level
319
- )
320
259
 
321
260
  render json: {
322
- nodes: nodes,
323
- creative_id: new_creative.id,
324
- parent_id: result.parent&.id,
325
- direction: result.direction
261
+ contexts: inherited + own,
262
+ can_manage: creative.has_permission?(Current.user, :admin),
263
+ disabled_self_context: creative.data&.dig("disabled_self_context") == true
326
264
  }
327
- rescue ::Creatives::Reorderer::Error
328
- head :unprocessable_entity
329
- end
330
-
331
- def append_as_parent
332
- @parent_creative = Creative.find_by(id: params[:parent_id]).parent
333
- redirect_to new_creative_path(parent_id: @parent_creative&.id, child_id: params[:parent_id], tags: params[:tags])
334
265
  end
335
266
 
336
- def append_below
337
- target = Creative.find_by(id: params[:creative_id])
338
- redirect_to new_creative_path(parent_id: target&.parent_id, after_id: target&.id, tags: params[:tags])
339
- end
340
-
341
- def children
342
- parent = Creative.find(params[:id])
343
- effective = parent.effective_origin
344
- # user_id for expanded_state lookup - use owner's state for anonymous users
345
- state_user_id = Current.user&.id || effective.user_id
346
-
347
- # HTTP caching disabled for children endpoint:
348
- # Response depends on child updates, permission changes (CreativeSharesCache),
349
- # and CreativeExpandedState. Tracking all dependencies reliably is expensive
350
- # (requires descendant_ids query). Stale 304 responses could leak data after
351
- # permission revocation. Re-enable when a cheap version key mechanism exists.
352
- # Use private + no-store to prevent any caching (proxy or browser).
353
- response.headers["Cache-Control"] = "private, no-store"
354
-
355
- has_filters = params[:tags].present? || params[:min_progress].present? || params[:max_progress].present?
356
- if has_filters
357
- result = ::Creatives::IndexQuery.new(user: Current.user, params: params.merge(id: params[:id])).call
358
- render_children_json(parent, state_user_id, result.allowed_creative_ids, result.progress_map)
359
- else
360
- render_children_json(parent, state_user_id, nil, nil)
267
+ def update_contexts
268
+ creative = @creative.effective_origin(Set.new)
269
+ unless creative.has_permission?(Current.user, :admin)
270
+ render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden
271
+ return
361
272
  end
362
- end
363
273
 
364
- def unconvert
365
- base_creative = @creative.effective_origin
366
- parent = base_creative.parent
367
- if parent.nil?
368
- render json: { error: t("collavre.creatives.index.unconvert_no_parent") }, status: :unprocessable_entity and return
274
+ current_data = (creative.data || {}).dup
275
+ current_data["context_ids"] = Array(params[:context_ids]).map(&:to_i) if params.key?(:context_ids)
276
+ current_data["disabled_context_ids"] = Array(params[:disabled_context_ids]).map(&:to_i) if params.key?(:disabled_context_ids)
277
+ current_data.delete("disabled_context_ids") if current_data["disabled_context_ids"]&.empty?
278
+ if params.key?(:disabled_self_context)
279
+ if ActiveModel::Type::Boolean.new.cast(params[:disabled_self_context])
280
+ current_data["disabled_self_context"] = true
281
+ else
282
+ current_data.delete("disabled_self_context")
283
+ end
369
284
  end
370
285
 
371
- unless parent.has_permission?(Current.user, :feedback)
372
- render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
286
+ if creative.update(data: current_data)
287
+ head :ok
288
+ else
289
+ render json: { errors: creative.errors.full_messages }, status: :unprocessable_entity
373
290
  end
291
+ end
374
292
 
375
- unless base_creative.has_permission?(Current.user, :admin)
376
- render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
293
+ def update_metadata
294
+ creative = @creative.effective_origin(Set.new)
295
+ unless creative.has_permission?(Current.user, :write)
296
+ render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden
297
+ return
377
298
  end
378
299
 
379
- markdown = helpers.render_creative_tree_markdown([ base_creative ])
380
- comment = nil
381
-
382
- ActiveRecord::Base.transaction do
383
- comment = parent.effective_origin.comments.create!(content: markdown, user: Current.user)
384
- base_creative.descendants.each(&:destroy!)
385
- base_creative.destroy!
300
+ new_data = begin
301
+ JSON.parse(params[:data])
302
+ rescue JSON::ParserError => e
303
+ render json: { error: "Invalid JSON: #{e.message}" }, status: :unprocessable_entity
304
+ return
386
305
  end
387
-
388
- render json: { comment_id: comment.id }, status: :created
389
- rescue ActiveRecord::RecordInvalid => e
390
- render json: { error: e.record.errors.full_messages.to_sentence }, status: :unprocessable_entity
391
- end
392
-
393
- def export_markdown
394
- creatives = if params[:parent_id]
395
- parent_creative = Creative.find(params[:parent_id])
396
- effective_origin = parent_creative.effective_origin
397
- unless parent_creative.has_permission?(Current.user, :read) &&
398
- effective_origin.has_permission?(Current.user, :read)
399
- render plain: t("collavre.creatives.errors.no_permission"), status: :forbidden and return
400
- end
401
- [ effective_origin ]
306
+ if creative.update(data: new_data)
307
+ head :ok
402
308
  else
403
- Creative.where(parent_id: nil).map(&:effective_origin).uniq.select do |creative|
404
- creative.has_permission?(Current.user, :read)
405
- end
309
+ render json: { errors: creative.errors.full_messages }, status: :unprocessable_entity
406
310
  end
311
+ end
407
312
 
408
- if creatives.empty?
409
- render plain: t("collavre.creatives.errors.no_permission"), status: :forbidden and return
313
+ def destroy
314
+ parent = @creative.parent
315
+ unless @creative.has_permission?(Current.user, :admin)
316
+ redirect_to @creative, alert: t("collavre.creatives.errors.no_permission") and return
410
317
  end
411
-
412
- markdown = helpers.render_creative_tree_markdown(creatives)
413
- send_data markdown, filename: "creatives.md", type: "text/markdown"
318
+ Creatives::DestroyService.new(
319
+ creative: @creative,
320
+ user: Current.user,
321
+ delete_with_children: params[:delete_with_children].present?
322
+ ).call
414
323
  end
415
324
 
416
325
  private
@@ -449,18 +358,6 @@ module Collavre
449
358
  params[:unassigned].present?
450
359
  end
451
360
 
452
- def build_slide_ids(node)
453
- return unless node.has_permission?(Current.user, :read)
454
-
455
- @slide_ids << node.id
456
- children = node.children.order(:sequence)
457
- if node.origin_id.present?
458
- linked_children = node.linked_children
459
- children = (children + linked_children).uniq.sort_by(&:sequence)
460
- end
461
- children.each { |child| build_slide_ids(child) }
462
- end
463
-
464
361
  def serialize_creatives(collection)
465
362
  if params[:simple].present?
466
363
  collection.map { |c| { id: c.id, description: c.effective_description(nil, false), progress: c.progress } }
@@ -473,27 +370,6 @@ module Collavre
473
370
  @reorderer ||= ::Creatives::Reorderer.new(user: Current.user)
474
371
  end
475
372
 
476
- def render_children_json(parent, user_id, allowed_ids, progress_map)
477
- expanded_state_map = CreativeExpandedState
478
- .where(user_id: user_id, creative_id: parent.id)
479
- .first&.expanded_status || {}
480
- children = parent.children_with_permission(Current.user)
481
-
482
- level = params[:level].to_i
483
- json_level = level.zero? ? 1 : level
484
- render json: {
485
- creatives: build_tree(
486
- children,
487
- params: params,
488
- expanded_state_map: expanded_state_map,
489
- level: json_level,
490
- select_mode: params[:select_mode] == "1",
491
- allowed_creative_ids: allowed_ids,
492
- progress_map: progress_map
493
- )
494
- }
495
- end
496
-
497
373
  def enforce_creatives_login_policy
498
374
  if SystemSetting.creatives_login_required?
499
375
  require_authentication
@@ -40,6 +40,9 @@ module Collavre
40
40
  tz = request.env["omniauth.params"] && request.env["omniauth.params"]["timezone"]
41
41
  user.update(timezone: tz) if tz.present? && user.timezone != tz
42
42
 
43
+ return_to = session[:return_to_after_authenticating]
44
+ reset_session
45
+ session[:return_to_after_authenticating] = return_to if return_to
43
46
  start_new_session_for(user)
44
47
  redirect_to after_authentication_url
45
48
  end
@@ -8,7 +8,8 @@ module Collavre
8
8
  permission = params[:permission] || :read
9
9
  invitation = Invitation.create!(inviter: Current.user,
10
10
  creative: creative,
11
- permission: permission)
11
+ permission: permission,
12
+ email: params[:email].presence)
12
13
  render json: { url: invite_url(token: invitation.generate_token_for(:invite)) }
13
14
  end
14
15
 
@@ -28,6 +28,9 @@ module Collavre
28
28
  if user.email_verified?
29
29
  user.reset_failed_login_attempts!
30
30
  handle_invitation_for(user) if params[:invite_token].present?
31
+ return_to = session[:return_to_after_authenticating]
32
+ reset_session
33
+ session[:return_to_after_authenticating] = return_to if return_to
31
34
  start_new_session_for user
32
35
  tz = params[:timezone]
33
36
  user.update(timezone: tz) if tz.present? && user.timezone != tz
@@ -3,10 +3,13 @@ module Collavre
3
3
  before_action :set_creative
4
4
 
5
5
  def index
6
- can_manage = @creative.has_permission?(Current.user, :admin) || @creative.user == Current.user
6
+ is_owner = @creative.user == Current.user
7
+ can_manage = @creative.has_permission?(Current.user, :admin) || is_owner
8
+ can_create_topic = can_manage || @creative.has_permission?(Current.user, :write)
7
9
  render json: {
8
10
  topics: @creative.topics.order(:created_at),
9
- can_manage: can_manage
11
+ can_manage: can_manage,
12
+ can_create_topic: can_create_topic
10
13
  }
11
14
  end
12
15
 
@@ -63,6 +66,40 @@ module Collavre
63
66
  head :no_content
64
67
  end
65
68
 
69
+ def move
70
+ unless @creative.has_permission?(Current.user, :admin) || @creative.user == Current.user
71
+ render json: { error: I18n.t("collavre.topics.no_permission") }, status: :forbidden and return
72
+ end
73
+
74
+ topic = @creative.topics.find(params[:id])
75
+ target_creative = Creative.find(params[:target_creative_id]).effective_origin
76
+
77
+ unless target_creative.has_permission?(Current.user, :write) || target_creative.user == Current.user
78
+ render json: { error: I18n.t("collavre.topics.move.no_target_permission") }, status: :forbidden and return
79
+ end
80
+
81
+ # Check for duplicate topic name in target creative
82
+ if target_creative.topics.where(name: topic.name).exists?
83
+ render json: { error: I18n.t("collavre.topics.move.duplicate_name", name: topic.name) }, status: :unprocessable_entity and return
84
+ end
85
+
86
+ Topic.transaction do
87
+ topic.comments.update_all(creative_id: target_creative.id)
88
+ topic.update!(creative: target_creative)
89
+ end
90
+
91
+ TopicsChannel.broadcast_to(
92
+ @creative,
93
+ { action: "deleted", topic_id: topic.id }
94
+ )
95
+ TopicsChannel.broadcast_to(
96
+ target_creative,
97
+ { action: "created", topic: topic.slice(:id, :name) }
98
+ )
99
+
100
+ render json: { success: true, topic: topic.slice(:id, :name), target_creative_id: target_creative.id }
101
+ end
102
+
66
103
  def reorder
67
104
  unless @creative.has_permission?(Current.user, :admin) || @creative.user == Current.user
68
105
  render json: { error: I18n.t("collavre.topics.no_permission") }, status: :forbidden and return