collavre 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/collavre/comment_versions.css +76 -0
- data/app/assets/stylesheets/collavre/comments_popup.css +347 -37
- data/app/assets/stylesheets/collavre/creatives.css +73 -1
- data/app/assets/stylesheets/collavre/org_chart.css +319 -0
- data/app/assets/stylesheets/collavre/popup.css +68 -1
- data/app/controllers/collavre/application_controller.rb +13 -0
- data/app/controllers/collavre/comments/versions_controller.rb +82 -0
- data/app/controllers/collavre/comments_controller.rb +14 -153
- data/app/controllers/collavre/concerns/exportable.rb +30 -0
- data/app/controllers/collavre/concerns/shareable.rb +28 -0
- data/app/controllers/collavre/concerns/slide_viewable.rb +37 -0
- data/app/controllers/collavre/concerns/tree_manageable.rb +141 -0
- data/app/controllers/collavre/creative_imports_controller.rb +6 -0
- data/app/controllers/collavre/creative_invitations_controller.rb +46 -0
- data/app/controllers/collavre/creative_plans_controller.rb +1 -1
- data/app/controllers/collavre/creative_shares_controller.rb +84 -14
- data/app/controllers/collavre/creatives_controller.rb +70 -194
- data/app/controllers/collavre/google_auth_controller.rb +3 -0
- data/app/controllers/collavre/invites_controller.rb +2 -1
- data/app/controllers/collavre/sessions_controller.rb +3 -0
- data/app/controllers/collavre/topics_controller.rb +39 -2
- data/app/controllers/collavre/users_controller.rb +5 -404
- data/app/controllers/concerns/collavre/comments/approval_actions.rb +108 -0
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +55 -0
- data/app/controllers/concerns/collavre/comments/conversion.rb +46 -0
- data/app/controllers/concerns/collavre/users_controller/admin_operations.rb +74 -0
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +119 -0
- data/app/controllers/concerns/collavre/users_controller/contact_management.rb +166 -0
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +102 -0
- data/app/controllers/concerns/collavre/users_controller/registration.rb +63 -0
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/helpers/collavre/creatives_helper.rb +12 -9
- data/app/helpers/collavre/navigation_helper.rb +1 -1
- data/app/javascript/collavre.js +0 -1
- data/app/javascript/controllers/comment_controller.js +33 -70
- data/app/javascript/controllers/comment_version_controller.js +164 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +305 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_selection.test.js +103 -0
- data/app/javascript/controllers/comments/__tests__/review_quotes_store.test.js +113 -0
- data/app/javascript/controllers/comments/contexts_controller.js +363 -0
- data/app/javascript/controllers/comments/form_controller.js +304 -13
- data/app/javascript/controllers/comments/list_controller.js +151 -62
- data/app/javascript/controllers/comments/popup_controller.js +66 -38
- data/app/javascript/controllers/comments/presence_controller.js +2 -10
- data/app/javascript/controllers/comments/review_quotes_store.js +189 -0
- data/app/javascript/controllers/comments/topics_controller.js +34 -10
- data/app/javascript/controllers/index.js +15 -1
- data/app/javascript/controllers/org_chart_controller.js +46 -0
- data/app/javascript/controllers/share_modal_controller.js +369 -0
- data/app/javascript/controllers/topic_search_controller.js +103 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +42 -1
- data/app/javascript/lib/api/creatives.js +12 -0
- data/app/javascript/lib/api/csrf_fetch.js +35 -0
- data/app/javascript/lib/api/drag_drop.js +17 -0
- data/app/javascript/modules/command_menu.js +40 -0
- data/app/javascript/modules/creative_row_editor.js +88 -0
- data/app/javascript/modules/slide_view.js +2 -1
- data/app/jobs/collavre/ai_agent_job.rb +42 -30
- data/app/jobs/collavre/compress_job.rb +92 -0
- data/app/models/collavre/comment.rb +36 -1
- data/app/models/collavre/comment_version.rb +15 -0
- data/app/models/collavre/creative/describable.rb +1 -1
- data/app/models/collavre/creative.rb +51 -0
- data/app/models/collavre/task.rb +30 -2
- data/app/models/collavre/user.rb +20 -3
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +68 -0
- data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +89 -0
- data/app/services/collavre/ai_agent/message_builder.rb +85 -6
- data/app/services/collavre/ai_agent/response_finalizer.rb +97 -0
- data/app/services/collavre/ai_agent/response_streamer.rb +56 -0
- data/app/services/collavre/ai_agent/review_handler.rb +18 -1
- data/app/services/collavre/ai_agent_service.rb +130 -183
- data/app/services/collavre/ai_client.rb +6 -0
- data/app/services/collavre/auto_theme_generator.rb +1 -1
- data/app/services/collavre/command_menu_service.rb +19 -0
- data/app/services/collavre/comments/command_processor.rb +3 -1
- data/app/services/collavre/comments/compress_command.rb +75 -0
- data/app/services/collavre/comments/concerns/workflow_support.rb +115 -0
- data/app/services/collavre/comments/work_command.rb +161 -0
- data/app/services/collavre/comments/workflow_executor.rb +276 -0
- data/app/services/collavre/creatives/plan_tagger.rb +14 -3
- data/app/services/collavre/creatives/tree_formatter.rb +53 -13
- data/app/services/collavre/gemini_parent_recommender.rb +4 -4
- data/app/services/collavre/orchestration/agent_context_builder.rb +1 -3
- data/app/services/collavre/orchestration/agent_orchestrator.rb +15 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +0 -19
- data/app/services/collavre/orchestration/scheduler.rb +3 -2
- data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
- data/app/services/collavre/system_events/dispatcher.rb +9 -0
- data/app/services/collavre/tools/creative_create_service.rb +1 -8
- data/app/services/collavre/tools/creative_import_service.rb +46 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +157 -96
- data/app/services/collavre/tools/creative_update_service.rb +1 -8
- data/app/services/collavre/tools/cron_list_service.rb +1 -1
- data/app/services/collavre/tools/description_normalizable.rb +16 -0
- data/app/views/collavre/comments/_comment.html.erb +25 -8
- data/app/views/collavre/comments/_comments_popup.html.erb +32 -5
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +13 -0
- data/app/views/collavre/creatives/_share_button.html.erb +4 -1
- data/app/views/collavre/creatives/_share_modal.html.erb +31 -1
- data/app/views/collavre/creatives/index.html.erb +5 -5
- data/app/views/collavre/creatives/slide_view.html.erb +1 -1
- data/app/views/collavre/users/{_contact_management.html.erb → _contact_list.html.erb} +4 -8
- data/app/views/collavre/users/_org_chart.html.erb +68 -0
- data/app/views/collavre/users/_org_chart_node.html.erb +169 -0
- data/app/views/collavre/users/new_ai.html.erb +9 -0
- data/app/views/collavre/users/show.html.erb +32 -8
- data/config/locales/comments.en.yml +57 -2
- data/config/locales/comments.ko.yml +57 -2
- data/config/locales/contacts.en.yml +31 -0
- data/config/locales/contacts.ko.yml +31 -0
- data/config/locales/contexts.en.yml +8 -0
- data/config/locales/contexts.ko.yml +8 -0
- data/config/locales/creatives.en.yml +6 -0
- data/config/locales/creatives.ko.yml +6 -0
- data/config/locales/users.en.yml +1 -0
- data/config/locales/users.ko.yml +1 -0
- data/config/routes.rb +14 -1
- data/db/migrate/20260220072200_add_workflow_fields_to_tasks.rb +12 -0
- data/db/migrate/20260223173533_add_review_type_to_comments.rb +5 -0
- data/db/migrate/20260225065200_create_comment_versions.rb +14 -0
- data/db/migrate/20260225074416_add_selected_version_id_to_comments.rb +7 -0
- data/lib/collavre/version.rb +1 -1
- metadata +47 -10
- data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +0 -91
- data/app/javascript/lib/lexical/action_text_attachment_node.js +0 -459
- data/app/javascript/lib/lexical/dom_attachment_utils.js +0 -66
- data/app/javascript/modules/share_modal.js +0 -76
- data/app/javascript/modules/share_user_popup.js +0 -77
- data/app/services/collavre/orchestration/self_reflection_evaluator.rb +0 -231
- data/app/views/collavre/comments/_presence_avatars.html.erb +0 -8
- data/app/views/collavre/creatives/_delete_button.html.erb +0 -12
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class CommentsController < ApplicationController
|
|
3
|
+
include Collavre::Comments::ApprovalActions
|
|
4
|
+
include Collavre::Comments::Conversion
|
|
5
|
+
include Collavre::Comments::BatchOperations
|
|
6
|
+
|
|
3
7
|
before_action :set_creative
|
|
4
8
|
before_action :set_comment, only: [ :destroy, :show, :update, :convert, :approve, :update_action ]
|
|
5
9
|
|
|
@@ -11,6 +15,9 @@ module Collavre
|
|
|
11
15
|
@creatives = []
|
|
12
16
|
@shared_list = @creative.all_shared_users
|
|
13
17
|
@auto_fullscreen = true
|
|
18
|
+
# Set params[:id] so the tree URL in creatives/index loads children of this
|
|
19
|
+
# creative instead of the root list.
|
|
20
|
+
params[:id] = @creative.id.to_s
|
|
14
21
|
# Prepend creatives prefix so partials like 'add_button' resolve to collavre/creatives/_add_button
|
|
15
22
|
lookup_context.prefixes.prepend "collavre/creatives"
|
|
16
23
|
render "collavre/creatives/index"
|
|
@@ -25,7 +32,7 @@ module Collavre
|
|
|
25
32
|
Current.user.id,
|
|
26
33
|
Current.user.id
|
|
27
34
|
)
|
|
28
|
-
scope = visible_scope.with_attached_images.includes(:topic, :comment_reactions)
|
|
35
|
+
scope = visible_scope.with_attached_images.includes(:topic, :comment_reactions, :comment_versions)
|
|
29
36
|
|
|
30
37
|
if params[:search].present?
|
|
31
38
|
search_term = ActiveRecord::Base.sanitize_sql_like(params[:search].to_s.strip.downcase)
|
|
@@ -191,7 +198,7 @@ module Collavre
|
|
|
191
198
|
)
|
|
192
199
|
end
|
|
193
200
|
end
|
|
194
|
-
@comment = Comment.with_attached_images.includes(:comment_reactions).find(@comment.id)
|
|
201
|
+
@comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
|
|
195
202
|
render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }, status: :created
|
|
196
203
|
else
|
|
197
204
|
render json: { errors: @comment.errors.full_messages }, status: :unprocessable_entity
|
|
@@ -206,7 +213,7 @@ module Collavre
|
|
|
206
213
|
end
|
|
207
214
|
|
|
208
215
|
if @comment.update(safe_params)
|
|
209
|
-
@comment = Comment.with_attached_images.includes(:comment_reactions).find(@comment.id)
|
|
216
|
+
@comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
|
|
210
217
|
render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }
|
|
211
218
|
else
|
|
212
219
|
render json: { errors: @comment.errors.full_messages }, status: :unprocessable_entity
|
|
@@ -248,132 +255,7 @@ module Collavre
|
|
|
248
255
|
end
|
|
249
256
|
end
|
|
250
257
|
|
|
251
|
-
def convert
|
|
252
|
-
unless can_convert_comment?
|
|
253
|
-
render json: { error: I18n.t("collavre.comments.convert_not_allowed") }, status: :forbidden and return
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
created_creatives = ::MarkdownImporter.import(
|
|
257
|
-
@comment.content,
|
|
258
|
-
parent: @creative,
|
|
259
|
-
user: @creative.user,
|
|
260
|
-
create_root: true
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
primary_creative = created_creatives.first
|
|
264
|
-
system_message = build_convert_system_message(primary_creative) if primary_creative
|
|
265
|
-
|
|
266
|
-
@comment.destroy
|
|
267
|
-
|
|
268
|
-
if system_message.present?
|
|
269
|
-
Current.set(session: nil) do
|
|
270
|
-
@creative.comments.create!(content: system_message, user: nil)
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
head :no_content
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def approve
|
|
278
|
-
status = @comment.approval_status(Current.user)
|
|
279
|
-
if status != :ok
|
|
280
|
-
error_key = case status
|
|
281
|
-
when :invalid_action_format then "collavre.comments.approve_invalid_format"
|
|
282
|
-
when :missing_action then "collavre.comments.approve_missing_action"
|
|
283
|
-
when :missing_approver then "collavre.comments.approve_missing_approver"
|
|
284
|
-
when :admin_required then "collavre.comments.approve_admin_required"
|
|
285
|
-
else "collavre.comments.approve_not_allowed"
|
|
286
|
-
end
|
|
287
|
-
http_status = case status
|
|
288
|
-
when :invalid_action_format, :missing_action, :missing_approver
|
|
289
|
-
:unprocessable_entity
|
|
290
|
-
else
|
|
291
|
-
:forbidden
|
|
292
|
-
end
|
|
293
|
-
render json: { error: I18n.t(error_key) }, status: http_status and return
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
begin
|
|
297
|
-
::Comments::ActionExecutor.new(comment: @comment, executor: Current.user).call
|
|
298
|
-
@comment = Comment.with_attached_images.includes(:comment_reactions).find(@comment.id)
|
|
299
|
-
render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }
|
|
300
|
-
rescue ::Comments::ActionExecutor::ExecutionError => e
|
|
301
|
-
render json: { error: e.message }, status: :unprocessable_entity
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def update_action
|
|
306
|
-
# Initial checks outside the lock
|
|
307
|
-
executed_error = false
|
|
308
|
-
update_success = false
|
|
309
|
-
approver_mismatch_error = false
|
|
310
|
-
status_error_key = nil
|
|
311
|
-
status_http_status = nil
|
|
312
|
-
validation_error_message = nil
|
|
313
|
-
|
|
314
|
-
@comment.with_lock do
|
|
315
|
-
@comment.reload
|
|
316
|
-
|
|
317
|
-
status_in_lock = @comment.approval_status(Current.user)
|
|
318
|
-
# Allow repairing invalid format if user is approver
|
|
319
|
-
if status_in_lock == :invalid_action_format
|
|
320
|
-
if @comment.approver_id == Current.user&.id
|
|
321
|
-
status_in_lock = :ok
|
|
322
|
-
else
|
|
323
|
-
status_in_lock = :not_allowed
|
|
324
|
-
end
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
if status_in_lock != :ok
|
|
328
|
-
approver_mismatch_error = true
|
|
329
|
-
status_error_key = case status_in_lock
|
|
330
|
-
when :invalid_action_format then "collavre.comments.approve_invalid_format"
|
|
331
|
-
when :missing_action then "collavre.comments.approve_missing_action"
|
|
332
|
-
when :missing_approver then "collavre.comments.approve_missing_approver"
|
|
333
|
-
when :admin_required then "collavre.comments.approve_admin_required"
|
|
334
|
-
else "collavre.comments.approve_not_allowed"
|
|
335
|
-
end
|
|
336
|
-
status_http_status = case status_in_lock
|
|
337
|
-
when :invalid_action_format, :missing_action, :missing_approver
|
|
338
|
-
:unprocessable_entity
|
|
339
|
-
else
|
|
340
|
-
:forbidden
|
|
341
|
-
end
|
|
342
|
-
elsif @comment.action_executed_at.present?
|
|
343
|
-
executed_error = true
|
|
344
|
-
else
|
|
345
|
-
action_payload = params.dig(:comment, :action)
|
|
346
|
-
if action_payload.blank?
|
|
347
|
-
validation_error_message = I18n.t("collavre.comments.approve_missing_action")
|
|
348
|
-
else
|
|
349
|
-
begin
|
|
350
|
-
validator = ::Comments::ActionValidator.new(comment: @comment)
|
|
351
|
-
parsed_payload = validator.validate!(action_payload)
|
|
352
|
-
normalized_action = JSON.pretty_generate(parsed_payload)
|
|
353
|
-
update_success = @comment.update(action: normalized_action)
|
|
354
|
-
rescue ::Comments::ActionValidator::ValidationError => e
|
|
355
|
-
validation_error_message = e.message
|
|
356
|
-
end
|
|
357
|
-
end
|
|
358
|
-
end
|
|
359
|
-
end
|
|
360
258
|
|
|
361
|
-
if approver_mismatch_error
|
|
362
|
-
render json: { error: I18n.t(status_error_key) }, status: status_http_status
|
|
363
|
-
elsif validation_error_message
|
|
364
|
-
render json: { error: validation_error_message }, status: :unprocessable_entity
|
|
365
|
-
elsif executed_error
|
|
366
|
-
render json: { error: I18n.t("collavre.comments.approve_already_executed") }, status: :unprocessable_entity
|
|
367
|
-
elsif update_success
|
|
368
|
-
@comment = Comment.with_attached_images.includes(:comment_reactions).find(@comment.id)
|
|
369
|
-
render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }
|
|
370
|
-
else
|
|
371
|
-
error_message = @comment.errors.full_messages.to_sentence.presence || I18n.t("collavre.comments.action_update_error")
|
|
372
|
-
render json: { error: error_message }, status: :unprocessable_entity
|
|
373
|
-
end
|
|
374
|
-
rescue ::Comments::ActionValidator::ValidationError => e
|
|
375
|
-
render json: { error: e.message }, status: :unprocessable_entity
|
|
376
|
-
end
|
|
377
259
|
|
|
378
260
|
def show
|
|
379
261
|
redirect_to creative_path(@creative, comment_id: @comment.id)
|
|
@@ -406,24 +288,14 @@ module Collavre
|
|
|
406
288
|
render json: CommandMenuService.new(user: Current.user).items
|
|
407
289
|
end
|
|
408
290
|
|
|
409
|
-
def move
|
|
410
|
-
result = CommentMoveService.new(creative: @creative, user: Current.user).call(
|
|
411
|
-
comment_ids: params[:comment_ids],
|
|
412
|
-
target_creative_id: params[:target_creative_id],
|
|
413
|
-
target_topic_id: params[:target_topic_id]
|
|
414
|
-
)
|
|
415
|
-
render json: result
|
|
416
|
-
rescue CommentMoveService::MoveError => e
|
|
417
|
-
status = e.message == I18n.t("collavre.comments.move_not_allowed") ? :forbidden : :unprocessable_entity
|
|
418
|
-
render json: { error: e.message }, status: status
|
|
419
|
-
rescue ActiveRecord::RecordInvalid => e
|
|
420
|
-
render json: { error: e.record.errors.full_messages.to_sentence.presence || I18n.t("collavre.comments.move_error") }, status: :unprocessable_entity
|
|
421
|
-
end
|
|
422
291
|
|
|
423
292
|
private
|
|
424
293
|
|
|
425
294
|
def set_creative
|
|
426
295
|
@creative = Creative.find(params[:creative_id]).effective_origin
|
|
296
|
+
unless @creative.has_permission?(Current.user, :read)
|
|
297
|
+
render json: { error: I18n.t("collavre.creatives.errors.no_permission") }, status: :forbidden
|
|
298
|
+
end
|
|
427
299
|
end
|
|
428
300
|
|
|
429
301
|
def set_comment
|
|
@@ -438,18 +310,7 @@ module Collavre
|
|
|
438
310
|
end
|
|
439
311
|
|
|
440
312
|
def comment_params
|
|
441
|
-
params.require(:comment).permit(:content, :private, :topic_id, :quoted_comment_id, :quoted_text, images: [])
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
def can_convert_comment?
|
|
445
|
-
@comment.user == Current.user || @creative.has_permission?(Current.user, :admin)
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
def build_convert_system_message(creative)
|
|
449
|
-
title = helpers.strip_tags(creative.description).to_s.strip
|
|
450
|
-
title = I18n.t("collavre.comments.convert_system_message_default_title") if title.blank?
|
|
451
|
-
url = creative_path(creative)
|
|
452
|
-
I18n.t("collavre.comments.convert_system_message", title: title, url: url)
|
|
313
|
+
params.require(:comment).permit(:content, :private, :topic_id, :quoted_comment_id, :quoted_text, :review_type, images: [])
|
|
453
314
|
end
|
|
454
315
|
|
|
455
316
|
def current_topic_context
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module Concerns
|
|
3
|
+
module Exportable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def export_markdown
|
|
7
|
+
creatives = if params[:parent_id]
|
|
8
|
+
parent_creative = Creative.find(params[:parent_id])
|
|
9
|
+
effective_origin = parent_creative.effective_origin
|
|
10
|
+
unless parent_creative.has_permission?(Current.user, :read) &&
|
|
11
|
+
effective_origin.has_permission?(Current.user, :read)
|
|
12
|
+
render plain: t("collavre.creatives.errors.no_permission"), status: :forbidden and return
|
|
13
|
+
end
|
|
14
|
+
[ effective_origin ]
|
|
15
|
+
else
|
|
16
|
+
Creative.where(parent_id: nil).map(&:effective_origin).uniq.select do |creative|
|
|
17
|
+
creative.has_permission?(Current.user, :read)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if creatives.empty?
|
|
22
|
+
render plain: t("collavre.creatives.errors.no_permission"), status: :forbidden and return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
markdown = helpers.render_creative_tree_markdown(creatives)
|
|
26
|
+
send_data markdown, filename: "creatives.md", type: "text/markdown"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module Concerns
|
|
3
|
+
module Shareable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def request_permission
|
|
7
|
+
creative = @creative.effective_origin
|
|
8
|
+
if creative.user == Current.user || creative.has_permission?(Current.user, :read)
|
|
9
|
+
return head :unprocessable_entity
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
short_title = helpers.strip_tags(creative.effective_origin.description).truncate(10)
|
|
13
|
+
|
|
14
|
+
InboxItem.create!(
|
|
15
|
+
owner: creative.user,
|
|
16
|
+
message_key: "inbox.permission_requested",
|
|
17
|
+
message_params: { user: Current.user.display_name, short_title: short_title },
|
|
18
|
+
link: creative_url(
|
|
19
|
+
creative,
|
|
20
|
+
Rails.application.config.action_mailer.default_url_options.merge(share_request: Current.user.email)
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
head :ok
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module Concerns
|
|
3
|
+
module SlideViewable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def slide_view
|
|
7
|
+
unless @creative.has_permission?(Current.user, :read)
|
|
8
|
+
if Current.user
|
|
9
|
+
redirect_to creatives_path, alert: t("collavre.creatives.errors.no_permission")
|
|
10
|
+
else
|
|
11
|
+
request_authentication
|
|
12
|
+
end
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@slide_ids = []
|
|
17
|
+
@root_depth = @creative.ancestors.count
|
|
18
|
+
build_slide_ids(@creative)
|
|
19
|
+
render layout: "collavre/slide"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build_slide_ids(node)
|
|
25
|
+
return unless node.has_permission?(Current.user, :read)
|
|
26
|
+
|
|
27
|
+
@slide_ids << node.id
|
|
28
|
+
children = node.children.order(:sequence)
|
|
29
|
+
if node.origin_id.present?
|
|
30
|
+
linked_children = node.linked_children
|
|
31
|
+
children = (children + linked_children).uniq.sort_by(&:sequence)
|
|
32
|
+
end
|
|
33
|
+
children.each { |child| build_slide_ids(child) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module Concerns
|
|
3
|
+
module TreeManageable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def reorder
|
|
7
|
+
dragged_ids = Array(params[:dragged_ids]).map(&:presence).compact
|
|
8
|
+
target_id = params[:target_id]
|
|
9
|
+
direction = params[:direction]
|
|
10
|
+
|
|
11
|
+
if dragged_ids.any?
|
|
12
|
+
reorderer.reorder_multiple(
|
|
13
|
+
dragged_ids: dragged_ids,
|
|
14
|
+
target_id: target_id,
|
|
15
|
+
direction: direction
|
|
16
|
+
)
|
|
17
|
+
else
|
|
18
|
+
reorderer.reorder(
|
|
19
|
+
dragged_id: params[:dragged_id],
|
|
20
|
+
target_id: target_id,
|
|
21
|
+
direction: direction
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
head :ok
|
|
25
|
+
rescue ::Creatives::Reorderer::Error
|
|
26
|
+
head :unprocessable_entity
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def link_drop
|
|
30
|
+
result = reorderer.link_drop(
|
|
31
|
+
dragged_id: params[:dragged_id],
|
|
32
|
+
target_id: params[:target_id],
|
|
33
|
+
direction: params[:direction]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
new_creative = result.new_creative
|
|
37
|
+
level = new_creative.ancestors.count + 1
|
|
38
|
+
nodes = build_tree(
|
|
39
|
+
[ new_creative ],
|
|
40
|
+
params: params,
|
|
41
|
+
expanded_state_map: {},
|
|
42
|
+
level: level
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
render json: {
|
|
46
|
+
nodes: nodes,
|
|
47
|
+
creative_id: new_creative.id,
|
|
48
|
+
parent_id: result.parent&.id,
|
|
49
|
+
direction: result.direction
|
|
50
|
+
}
|
|
51
|
+
rescue ::Creatives::Reorderer::Error
|
|
52
|
+
head :unprocessable_entity
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def append_as_parent
|
|
56
|
+
@parent_creative = Creative.find_by(id: params[:parent_id]).parent
|
|
57
|
+
redirect_to new_creative_path(parent_id: @parent_creative&.id, child_id: params[:parent_id], tags: params[:tags])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def append_below
|
|
61
|
+
target = Creative.find_by(id: params[:creative_id])
|
|
62
|
+
redirect_to new_creative_path(parent_id: target&.parent_id, after_id: target&.id, tags: params[:tags])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def children
|
|
66
|
+
parent = Creative.find(params[:id])
|
|
67
|
+
effective = parent.effective_origin
|
|
68
|
+
# user_id for expanded_state lookup - use owner's state for anonymous users
|
|
69
|
+
state_user_id = Current.user&.id || effective.user_id
|
|
70
|
+
|
|
71
|
+
# HTTP caching disabled for children endpoint:
|
|
72
|
+
# Response depends on child updates, permission changes (CreativeSharesCache),
|
|
73
|
+
# and CreativeExpandedState. Tracking all dependencies reliably is expensive
|
|
74
|
+
# (requires descendant_ids query). Stale 304 responses could leak data after
|
|
75
|
+
# permission revocation. Re-enable when a cheap version key mechanism exists.
|
|
76
|
+
# Use private + no-store to prevent any caching (proxy or browser).
|
|
77
|
+
response.headers["Cache-Control"] = "private, no-store"
|
|
78
|
+
|
|
79
|
+
has_filters = params[:tags].present? || params[:min_progress].present? || params[:max_progress].present?
|
|
80
|
+
if has_filters
|
|
81
|
+
result = ::Creatives::IndexQuery.new(user: Current.user, params: params.merge(id: params[:id])).call
|
|
82
|
+
render_children_json(parent, state_user_id, result.allowed_creative_ids, result.progress_map)
|
|
83
|
+
else
|
|
84
|
+
render_children_json(parent, state_user_id, nil, nil)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def unconvert
|
|
89
|
+
base_creative = @creative.effective_origin
|
|
90
|
+
parent = base_creative.parent
|
|
91
|
+
if parent.nil?
|
|
92
|
+
render json: { error: t("collavre.creatives.index.unconvert_no_parent") }, status: :unprocessable_entity and return
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
unless parent.has_permission?(Current.user, :feedback)
|
|
96
|
+
render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
unless base_creative.has_permission?(Current.user, :admin)
|
|
100
|
+
render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
markdown = helpers.render_creative_tree_markdown([ base_creative ])
|
|
104
|
+
comment = nil
|
|
105
|
+
|
|
106
|
+
ActiveRecord::Base.transaction do
|
|
107
|
+
comment = parent.effective_origin.comments.create!(content: markdown, user: Current.user)
|
|
108
|
+
base_creative.descendants.each(&:destroy!)
|
|
109
|
+
base_creative.destroy!
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
render json: { comment_id: comment.id }, status: :created
|
|
113
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
114
|
+
render json: { error: e.record.errors.full_messages.to_sentence }, status: :unprocessable_entity
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def render_children_json(parent, user_id, allowed_ids, progress_map)
|
|
120
|
+
expanded_state_map = CreativeExpandedState
|
|
121
|
+
.where(user_id: user_id, creative_id: parent.id)
|
|
122
|
+
.first&.expanded_status || {}
|
|
123
|
+
children = parent.children_with_permission(Current.user)
|
|
124
|
+
|
|
125
|
+
level = params[:level].to_i
|
|
126
|
+
json_level = level.zero? ? 1 : level
|
|
127
|
+
render json: {
|
|
128
|
+
creatives: build_tree(
|
|
129
|
+
children,
|
|
130
|
+
params: params,
|
|
131
|
+
expanded_state_map: expanded_state_map,
|
|
132
|
+
level: json_level,
|
|
133
|
+
select_mode: params[:select_mode] == "1",
|
|
134
|
+
allowed_creative_ids: allowed_ids,
|
|
135
|
+
progress_map: progress_map
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class CreativeImportsController < ApplicationController
|
|
3
|
+
# Allow unauthenticated access so we can return JSON 401 instead of HTML redirect
|
|
4
|
+
# for API/fetch callers (import_controller.js uses csrfFetch with Accept: application/json)
|
|
3
5
|
allow_unauthenticated_access only: :create
|
|
4
6
|
|
|
5
7
|
def create
|
|
@@ -8,6 +10,10 @@ module Collavre
|
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
parent = params[:parent_id].present? ? Creative.find_by(id: params[:parent_id]) : nil
|
|
13
|
+
if parent && !parent.has_permission?(Current.user, :write)
|
|
14
|
+
render json: { error: I18n.t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
created = ::Creatives::Importer.new(file: params[:markdown], user: Current.user, parent: parent).call
|
|
12
18
|
|
|
13
19
|
if created.any?
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
class CreativeInvitationsController < ApplicationController
|
|
5
|
+
before_action :set_invitation
|
|
6
|
+
before_action :authorize_admin!
|
|
7
|
+
|
|
8
|
+
def update
|
|
9
|
+
if @invitation.update(permission: params[:permission])
|
|
10
|
+
respond_to do |format|
|
|
11
|
+
format.html { redirect_back fallback_location: main_app.root_path, notice: t("collavre.creatives.share.permission_updated") }
|
|
12
|
+
format.json { render json: { permission: @invitation.permission }, status: :ok }
|
|
13
|
+
end
|
|
14
|
+
else
|
|
15
|
+
respond_to do |format|
|
|
16
|
+
format.html { redirect_back fallback_location: main_app.root_path, alert: @invitation.errors.full_messages.to_sentence }
|
|
17
|
+
format.json { render json: { error: @invitation.errors.full_messages.to_sentence }, status: :unprocessable_entity }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def destroy
|
|
23
|
+
@invitation.destroy
|
|
24
|
+
respond_to do |format|
|
|
25
|
+
format.html { redirect_back fallback_location: main_app.root_path, notice: t("collavre.contacts.org_chart.invite_cancelled") }
|
|
26
|
+
format.json { head :no_content }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def set_invitation
|
|
33
|
+
@invitation = Invitation.find(params[:id])
|
|
34
|
+
@creative = @invitation.creative
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def authorize_admin!
|
|
38
|
+
return if @creative.has_permission?(Current.user, :admin)
|
|
39
|
+
|
|
40
|
+
respond_to do |format|
|
|
41
|
+
format.html { redirect_back fallback_location: main_app.root_path, alert: t("collavre.creatives.errors.no_permission") }
|
|
42
|
+
format.json { render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -51,7 +51,7 @@ module Collavre
|
|
|
51
51
|
private
|
|
52
52
|
|
|
53
53
|
def tagger
|
|
54
|
-
::Creatives::PlanTagger.new(plan_id: params[:plan_id], creative_ids: parsed_creative_ids)
|
|
54
|
+
::Creatives::PlanTagger.new(plan_id: params[:plan_id], creative_ids: parsed_creative_ids, user: Current.user)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def parsed_creative_ids
|
|
@@ -1,22 +1,52 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class CreativeSharesController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
@creative = Creative.find(params[:creative_id])
|
|
5
|
+
unless @creative.has_permission?(Current.user, :admin)
|
|
6
|
+
head :forbidden and return
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
@shared_list = CreativeShare.where(creative: @creative)
|
|
10
|
+
.includes(user: [ avatar_attachment: :blob ])
|
|
11
|
+
|
|
12
|
+
@pending_invitations = Invitation.where(creative: @creative, accepted_at: nil)
|
|
13
|
+
.where("expires_at > ?", Time.current)
|
|
14
|
+
.order(created_at: :desc)
|
|
15
|
+
|
|
16
|
+
render partial: "collavre/creatives/share_modal", layout: false
|
|
17
|
+
end
|
|
18
|
+
|
|
3
19
|
def create
|
|
4
20
|
@creative = Creative.find(params[:creative_id]).effective_origin
|
|
5
21
|
|
|
22
|
+
unless @creative.has_permission?(Current.user, :admin)
|
|
23
|
+
respond_to do |format|
|
|
24
|
+
format.html { redirect_back fallback_location: creatives_path, alert: t("collavre.creatives.errors.no_permission") }
|
|
25
|
+
format.json { render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden }
|
|
26
|
+
end
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
6
30
|
user = nil
|
|
7
31
|
if params[:user_email].present?
|
|
8
32
|
user = User.find_by(email: params[:user_email])
|
|
9
33
|
unless user
|
|
10
34
|
invitation = Invitation.create!(email: params[:user_email], inviter: Current.user, creative: @creative, permission: params[:permission])
|
|
11
35
|
InvitationMailer.with(invitation: invitation).invite.deliver_later
|
|
12
|
-
|
|
13
|
-
|
|
36
|
+
respond_to do |format|
|
|
37
|
+
format.html { redirect_back fallback_location: creatives_path, notice: t("collavre.invites.invite_sent") }
|
|
38
|
+
format.json { render json: { notice: t("collavre.invites.invite_sent") }, status: :created }
|
|
39
|
+
end
|
|
40
|
+
return
|
|
14
41
|
end
|
|
15
42
|
|
|
16
43
|
# Restrict sharing non-searchable AI agents to their owners only
|
|
17
44
|
if user.ai_user? && !user.searchable? && user.created_by_id != Current.user.id
|
|
18
|
-
|
|
19
|
-
|
|
45
|
+
respond_to do |format|
|
|
46
|
+
format.html { redirect_back fallback_location: creatives_path, alert: t("collavre.creatives.share.cannot_share_private_ai_agent") }
|
|
47
|
+
format.json { render json: { error: t("collavre.creatives.share.cannot_share_private_ai_agent") }, status: :forbidden }
|
|
48
|
+
end
|
|
49
|
+
return
|
|
20
50
|
end
|
|
21
51
|
end
|
|
22
52
|
|
|
@@ -36,14 +66,18 @@ module Collavre
|
|
|
36
66
|
Rails.logger.debug "### closest_parent_share = #{closest_parent_share.inspect}, is_param_no_access: #{is_param_no_access}"
|
|
37
67
|
if closest_parent_share.present?
|
|
38
68
|
if closest_parent_share.permission == :no_access.to_s
|
|
39
|
-
|
|
40
|
-
|
|
69
|
+
respond_to do |format|
|
|
70
|
+
format.html { redirect_back fallback_location: creatives_path, alert: t("collavre.creatives.share.can_not_share_by_no_access_in_parent") }
|
|
71
|
+
format.json { render json: { error: t("collavre.creatives.share.can_not_share_by_no_access_in_parent") }, status: :unprocessable_entity }
|
|
72
|
+
end
|
|
73
|
+
return
|
|
41
74
|
else
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
unless is_param_no_access
|
|
76
|
+
respond_to do |format|
|
|
77
|
+
format.html { redirect_back fallback_location: creatives_path, alert: t("collavre.creatives.share.already_shared_in_parent") }
|
|
78
|
+
format.json { render json: { error: t("collavre.creatives.share.already_shared_in_parent") }, status: :unprocessable_entity }
|
|
79
|
+
end
|
|
80
|
+
return
|
|
47
81
|
end
|
|
48
82
|
end
|
|
49
83
|
end
|
|
@@ -57,15 +91,51 @@ module Collavre
|
|
|
57
91
|
Contact.ensure(user: Current.user, contact_user: user)
|
|
58
92
|
Contact.ensure(user: @creative.user, contact_user: user)
|
|
59
93
|
end
|
|
60
|
-
|
|
94
|
+
respond_to do |format|
|
|
95
|
+
format.html { redirect_back fallback_location: creatives_path, notice: t("collavre.creatives.share.shared") }
|
|
96
|
+
format.json { render json: { notice: t("collavre.creatives.share.shared") }, status: :created }
|
|
97
|
+
end
|
|
61
98
|
else
|
|
62
|
-
|
|
99
|
+
respond_to do |format|
|
|
100
|
+
format.html { redirect_back fallback_location: creatives_path, alert: share.errors.full_messages.to_sentence }
|
|
101
|
+
format.json { render json: { error: share.errors.full_messages.to_sentence }, status: :unprocessable_entity }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def update
|
|
107
|
+
@creative_share = CreativeShare.find(params[:id])
|
|
108
|
+
unless @creative_share.creative.has_permission?(Current.user, :admin)
|
|
109
|
+
respond_to do |format|
|
|
110
|
+
format.html { redirect_back fallback_location: main_app.root_path, alert: t("collavre.creatives.errors.no_permission") }
|
|
111
|
+
format.json { render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden }
|
|
112
|
+
end
|
|
113
|
+
return
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if @creative_share.update(permission: params[:permission])
|
|
117
|
+
respond_to do |format|
|
|
118
|
+
format.html { redirect_back fallback_location: main_app.root_path, notice: t("collavre.creatives.share.permission_updated") }
|
|
119
|
+
format.json { render json: { permission: @creative_share.permission }, status: :ok }
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
respond_to do |format|
|
|
123
|
+
format.html { redirect_back fallback_location: main_app.root_path, alert: @creative_share.errors.full_messages.to_sentence }
|
|
124
|
+
format.json { render json: { error: @creative_share.errors.full_messages.to_sentence }, status: :unprocessable_entity }
|
|
125
|
+
end
|
|
63
126
|
end
|
|
64
|
-
redirect_back(fallback_location: creatives_path)
|
|
65
127
|
end
|
|
66
128
|
|
|
67
129
|
def destroy
|
|
68
130
|
@creative_share = CreativeShare.find(params[:id])
|
|
131
|
+
unless @creative_share.creative.has_permission?(Current.user, :admin)
|
|
132
|
+
respond_to do |format|
|
|
133
|
+
format.html { redirect_back fallback_location: main_app.root_path, alert: t("collavre.creatives.errors.no_permission") }
|
|
134
|
+
format.json { render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden }
|
|
135
|
+
end
|
|
136
|
+
return
|
|
137
|
+
end
|
|
138
|
+
|
|
69
139
|
@creative_share.destroy
|
|
70
140
|
# remove linked creative if it exists
|
|
71
141
|
linked_creative = Creative.find_by(origin_id: @creative_share.creative_id, user_id: @creative_share.user_id)
|