collavre 0.16.0 → 0.20.1

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/comments_popup.css +65 -5
  3. data/app/assets/stylesheets/collavre/creatives.css +5 -2
  4. data/app/controllers/collavre/admin/settings_controller.rb +9 -0
  5. data/app/controllers/collavre/attachments_controller.rb +1 -1
  6. data/app/controllers/collavre/comment_read_pointers_controller.rb +2 -2
  7. data/app/controllers/collavre/comments/snapshots_controller.rb +2 -7
  8. data/app/controllers/collavre/comments_controller.rb +23 -25
  9. data/app/controllers/collavre/creatives_controller.rb +22 -11
  10. data/app/controllers/collavre/emails_controller.rb +2 -0
  11. data/app/controllers/collavre/google_auth_controller.rb +1 -1
  12. data/app/controllers/collavre/inbox_items_controller.rb +1 -1
  13. data/app/controllers/collavre/invites_controller.rb +3 -0
  14. data/app/controllers/collavre/tasks_controller.rb +1 -1
  15. data/app/controllers/collavre/topics_controller.rb +27 -85
  16. data/app/controllers/concerns/collavre/comments/comment_scoping.rb +3 -0
  17. data/app/controllers/concerns/collavre/creative_permission_guard.rb +32 -0
  18. data/app/controllers/concerns/collavre/integration_setup.rb +17 -0
  19. data/app/helpers/collavre/application_helper.rb +30 -2
  20. data/app/javascript/components/InlineLexicalEditor.jsx +7 -3
  21. data/app/javascript/components/creative_tree_row.js +21 -1
  22. data/app/javascript/components/plugins/markdown_shortcuts_plugin.jsx +34 -0
  23. data/app/javascript/controllers/comment_controller.js +17 -0
  24. data/app/javascript/controllers/comments/form_controller.js +7 -4
  25. data/app/javascript/controllers/comments/list_controller.js +43 -4
  26. data/app/javascript/controllers/comments/popup_controller.js +45 -12
  27. data/app/javascript/controllers/comments/presence_controller.js +8 -0
  28. data/app/javascript/controllers/comments/topics_controller.js +50 -31
  29. data/app/javascript/creatives/tree_renderer.js +1 -0
  30. data/app/javascript/lib/__tests__/chat_history.test.js +31 -0
  31. data/app/javascript/lib/chat_history.js +12 -2
  32. data/app/javascript/modules/command_args_form.js +8 -0
  33. data/app/javascript/modules/creative_row_editor.js +12 -17
  34. data/app/javascript/modules/integration_wizard.js +162 -0
  35. data/app/jobs/collavre/compress_job.rb +1 -0
  36. data/app/jobs/collavre/creative_broadcast_job.rb +4 -1
  37. data/app/jobs/collavre/merge_comments_job.rb +1 -0
  38. data/app/jobs/collavre/trigger_loop_check_job.rb +1 -0
  39. data/app/jobs/collavre/trigger_loop_verify_job.rb +1 -0
  40. data/app/models/collavre/calendar_event.rb +0 -4
  41. data/app/models/collavre/comment/broadcastable.rb +1 -1
  42. data/app/models/collavre/comment.rb +17 -2
  43. data/app/models/collavre/comment_snapshot.rb +0 -1
  44. data/app/models/collavre/creative/describable.rb +10 -1
  45. data/app/models/collavre/creative/realtime_broadcastable.rb +17 -5
  46. data/app/models/collavre/creative.rb +43 -1
  47. data/app/models/collavre/current.rb +1 -1
  48. data/app/models/collavre/inbox_item.rb +0 -4
  49. data/app/models/collavre/system_setting.rb +10 -1
  50. data/app/models/collavre/task.rb +17 -8
  51. data/app/models/collavre/user.rb +11 -1
  52. data/app/models/concerns/collavre/ai_agent_resolvable.rb +0 -8
  53. data/app/services/collavre/ai_agent/message_builder.rb +32 -15
  54. data/app/services/collavre/ai_agent/response_finalizer.rb +2 -1
  55. data/app/services/collavre/ai_agent/session_context_resolver.rb +50 -0
  56. data/app/services/collavre/ai_agent_service.rb +14 -2
  57. data/app/services/collavre/ai_client.rb +13 -0
  58. data/app/services/collavre/command_menu_service.rb +23 -3
  59. data/app/services/collavre/comments/command_processor.rb +1 -1
  60. data/app/services/collavre/comments/mcp_command.rb +27 -8
  61. data/app/services/collavre/comments/mcp_command_builder.rb +4 -3
  62. data/app/services/collavre/creatives/tree_builder.rb +3 -0
  63. data/app/services/collavre/google_calendar_service.rb +32 -6
  64. data/app/services/collavre/markdown_converter.rb +15 -20
  65. data/app/services/collavre/mcp_service.rb +4 -4
  66. data/app/services/collavre/orchestration/agent_orchestrator.rb +2 -2
  67. data/app/services/collavre/tools/creative_batch_service.rb +5 -1
  68. data/app/services/collavre/tools/cron_create_service.rb +9 -6
  69. data/app/services/collavre/topic_branch_service.rb +2 -2
  70. data/app/views/collavre/admin/settings/_system_tab.html.erb +11 -0
  71. data/app/views/collavre/comments/_comment.html.erb +7 -1
  72. data/app/views/collavre/comments/_comments_popup.html.erb +4 -1
  73. data/config/locales/admin.en.yml +3 -0
  74. data/config/locales/admin.ko.yml +3 -0
  75. data/config/locales/comments.en.yml +6 -1
  76. data/config/locales/comments.ko.yml +6 -1
  77. data/db/migrate/20251126040752_add_description_to_creatives.rb +1 -1
  78. data/db/migrate/20260415000000_create_main_topics_for_existing_creatives.rb +69 -0
  79. data/db/migrate/20260415094811_add_task_id_to_comments.rb +6 -0
  80. data/lib/collavre/version.rb +1 -1
  81. metadata +22 -4
  82. data/app/jobs/collavre/permission_cache_cleanup_job.rb +0 -36
  83. data/app/models/collavre/variation.rb +0 -5
  84. data/app/services/collavre/creatives/path_exporter.rb +0 -131
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6bf5cb5904041bc150792f274d44ce65f7b659bebf346a0f27dc00a1cbc0067
4
- data.tar.gz: 33a8b19ecb1e5e2a02a8144ef7038d825c8d0d2cc9cd7195ea7fdc21230f9cd3
3
+ metadata.gz: 85ccc8b054af6b7b505ec32e773367299bc72d4cc3dd0d13fc345048b358470b
4
+ data.tar.gz: 1ebb968d3a1db4f5fe72bb67afca30154d705a0c0e682cbc2aea1a830e092c4f
5
5
  SHA512:
6
- metadata.gz: 95e7f7877a88161151e55b26d9a46503a3889ed940443ba9436dc7fff063fc75cdd7ae554bc6a5c144bd5246dbf1822476fde15a925663d040ae6b54e59b6af3
7
- data.tar.gz: 443e491e30ec68060e75b18a4f3abce13b764ce23d049b0f9be4081f322b99964313f29d46cb4d477f93aa75275bfb8dec7870bfb56dc74699fea747cb9e160c
6
+ metadata.gz: a6a9b9f7068b3f9f14eb680ee758e35031fef2e406994b462ce0ca22609bb59a6f96d956d51c0e28041f9ee46205a4b8360145fba434942103b819d50e63e9f2
7
+ data.tar.gz: 6354dc440494f9f7c1753de7909ce7d04ec24dc7d2d9557f0180dfdd09deaa6171af0c1c96e9d5abb21a4164c55ab7e3efa0293e3a4d6a280c54e479b6ed8cdd
@@ -2,7 +2,7 @@
2
2
  display: none;
3
3
  position: fixed;
4
4
  right: 2em;
5
- /* top: 10em; */
5
+ top: 10em;
6
6
  z-index: var(--layer-modal);
7
7
  width: 420px;
8
8
  height: 640px;
@@ -19,12 +19,20 @@
19
19
  flex-direction: column;
20
20
  transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease,
21
21
  right 0.25s ease, bottom 0.25s ease, border-radius 0.25s ease,
22
- box-shadow 0.25s ease, padding 0.25s ease;
22
+ box-shadow 0.25s ease, padding 0.25s ease, opacity 0.3s ease;
23
23
  max-width: calc(100vw - 0.5em) !important;
24
24
  overscroll-behavior: contain;
25
25
  overflow: hidden;
26
26
  }
27
27
 
28
+ #comments-popup.editor-behind {
29
+ opacity: 0.15;
30
+ }
31
+
32
+ #comments-popup.editor-behind:hover {
33
+ opacity: 1;
34
+ }
35
+
28
36
  body.chat-fullscreen {
29
37
  overflow: hidden;
30
38
  }
@@ -918,6 +926,32 @@ body.chat-fullscreen {
918
926
  display: none;
919
927
  }
920
928
 
929
+ .comment-stop-btn {
930
+ background: var(--surface-btn);
931
+ border: 1px solid var(--border-color);
932
+ border-radius: var(--radius-2);
933
+ cursor: pointer;
934
+ padding: var(--space-px-1) var(--space-px-2);
935
+ font-size: var(--text-00);
936
+ color: var(--color-danger);
937
+ line-height: 1;
938
+ white-space: nowrap;
939
+ display: inline-flex;
940
+ align-items: center;
941
+ gap: var(--space-px-1);
942
+ transition: background 0.15s, color 0.15s;
943
+ }
944
+
945
+ .comment-stop-btn:hover {
946
+ background: var(--color-danger);
947
+ color: var(--text-on-btn);
948
+ }
949
+
950
+ .comment-stop-btn:disabled {
951
+ opacity: 0.5;
952
+ cursor: not-allowed;
953
+ }
954
+
921
955
  .comment-copy-notice {
922
956
  position: absolute;
923
957
  right: 0.2em;
@@ -1069,10 +1103,36 @@ body.chat-fullscreen {
1069
1103
  color: var(--color-text);
1070
1104
  }
1071
1105
 
1106
+ #typing-indicator-row {
1107
+ display: flex;
1108
+ align-items: center;
1109
+ min-height: var(--space-px-5);
1110
+ margin: 0.3em 0;
1111
+ gap: var(--space-1);
1112
+ }
1113
+
1114
+ .scroll-prev-msg-btn {
1115
+ background: none;
1116
+ border: 1px solid var(--border-color);
1117
+ border-radius: var(--radius-2);
1118
+ color: var(--text-muted);
1119
+ cursor: pointer;
1120
+ font-size: var(--text-1);
1121
+ line-height: 1;
1122
+ padding: var(--space-px-1) var(--space-2);
1123
+ flex-shrink: 0;
1124
+ transition: background 0.15s, color 0.15s;
1125
+ }
1126
+
1127
+ .scroll-prev-msg-btn:hover {
1128
+ background: var(--color-section-bg);
1129
+ color: var(--color-text);
1130
+ }
1131
+
1072
1132
  #typing-indicator {
1073
1133
  font-style: italic;
1074
- margin: 0.3em 0;
1075
- min-height: 24px;
1134
+ flex: 1;
1135
+ min-height: var(--space-px-5);
1076
1136
  display: flex;
1077
1137
  align-items: center;
1078
1138
  }
@@ -1101,7 +1161,7 @@ body.chat-fullscreen {
1101
1161
  background: var(--surface-btn);
1102
1162
  border: 1px solid var(--border-color);
1103
1163
  border-radius: var(--radius-2);
1104
- color: var(--text-muted);
1164
+ color: var(--color-danger);
1105
1165
  cursor: pointer;
1106
1166
  font-size: var(--text-00);
1107
1167
  padding: var(--space-px-1) var(--space-px-2);
@@ -364,13 +364,15 @@ creative-tree-row.show-edit .creative-row {
364
364
 
365
365
  .creative-row:hover .progress-toggle-wrap .creative-progress-complete,
366
366
  .creative-row:hover .progress-toggle-wrap .creative-progress-incomplete {
367
- display: none;
367
+ visibility: hidden;
368
368
  }
369
369
 
370
370
  .creative-row:hover .progress-toggle-checkbox {
371
371
  visibility: visible;
372
372
  opacity: 1;
373
- position: relative;
373
+ position: absolute;
374
+ inset: 0;
375
+ margin: auto;
374
376
  pointer-events: auto;
375
377
  accent-color: var(--color-brand);
376
378
  }
@@ -1040,3 +1042,4 @@ creative-tree-row.is-being-edited .creative-tree {
1040
1042
  justify-content: center;
1041
1043
  box-shadow: var(--shadow-2);
1042
1044
  }
1045
+
@@ -21,6 +21,9 @@ module Collavre
21
21
  # Session timeout settings
22
22
  @session_timeout_minutes = SystemSetting.session_timeout_minutes
23
23
 
24
+ # LLM settings
25
+ @llm_request_timeout_seconds = SystemSetting.llm_request_timeout_seconds
26
+
24
27
  # Rate limiting settings
25
28
  @password_reset_rate_limit = SystemSetting.password_reset_rate_limit
26
29
  @password_reset_rate_period_minutes = SystemSetting.password_reset_rate_period_minutes
@@ -143,6 +146,11 @@ module Collavre
143
146
  api_period = SystemSetting::DEFAULT_API_RATE_PERIOD_MINUTES if api_period < 1
144
147
  SystemSetting.find_or_initialize_by(key: "api_rate_period_minutes").tap { |s| s.value = api_period.to_s; s.save! }
145
148
 
149
+ # LLM Settings
150
+ llm_timeout = params[:llm_request_timeout_seconds].to_i
151
+ llm_timeout = SystemSetting::DEFAULT_LLM_REQUEST_TIMEOUT_SECONDS if llm_timeout < 30
152
+ SystemSetting.find_or_initialize_by(key: "llm_request_timeout_seconds").tap { |s| s.value = llm_timeout.to_s; s.save! }
153
+
146
154
  # Auth Providers
147
155
  auth_providers = Array(params[:auth_providers]).reject(&:blank?)
148
156
  if auth_providers.empty?
@@ -171,6 +179,7 @@ module Collavre
171
179
  @password_reset_rate_period_minutes = params[:password_reset_rate_period_minutes].to_i.positive? ? params[:password_reset_rate_period_minutes].to_i : SystemSetting::DEFAULT_PASSWORD_RESET_RATE_PERIOD_MINUTES
172
180
  @api_rate_limit = params[:api_rate_limit].to_i.positive? ? params[:api_rate_limit].to_i : SystemSetting::DEFAULT_API_RATE_LIMIT
173
181
  @api_rate_period_minutes = params[:api_rate_period_minutes].to_i.positive? ? params[:api_rate_period_minutes].to_i : SystemSetting::DEFAULT_API_RATE_PERIOD_MINUTES
182
+ @llm_request_timeout_seconds = params[:llm_request_timeout_seconds].to_i.positive? ? params[:llm_request_timeout_seconds].to_i : SystemSetting::DEFAULT_LLM_REQUEST_TIMEOUT_SECONDS
174
183
  @enabled_auth_providers = params[:auth_providers] || []
175
184
  render :index, status: :unprocessable_entity
176
185
  end
@@ -12,7 +12,7 @@ module Collavre
12
12
  head :no_content
13
13
  rescue ActiveRecord::RecordNotFound
14
14
  head :not_found
15
- rescue => e
15
+ rescue StandardError => e
16
16
  Rails.logger.error("Failed to delete attachment: #{e.message}")
17
17
  head :internal_server_error
18
18
  end
@@ -44,11 +44,11 @@ module Collavre
44
44
  end
45
45
 
46
46
  def find_nearest_public_comment_id(creative, comment_id)
47
- creative.comments.where(private: false).where("id <= ?", comment_id).maximum(:id)
47
+ creative.comments.public_only.where("id <= ?", comment_id).maximum(:id)
48
48
  end
49
49
 
50
50
  def fetch_users_on_effective_id(creative, effective_id)
51
- next_public_id = creative.comments.where(private: false).where("id > ?", effective_id).minimum(:id)
51
+ next_public_id = creative.comments.public_only.where("id > ?", effective_id).minimum(:id)
52
52
 
53
53
  query = CommentReadPointer.where(creative: creative)
54
54
  .where("last_read_comment_id >= ?", effective_id)
@@ -3,6 +3,8 @@
3
3
  module Collavre
4
4
  module Comments
5
5
  class SnapshotsController < ApplicationController
6
+ include Collavre::Comments::CommentScoping
7
+
6
8
  before_action :set_creative
7
9
  before_action :set_snapshot, only: [ :restore ]
8
10
 
@@ -29,13 +31,6 @@ module Collavre
29
31
 
30
32
  private
31
33
 
32
- def set_creative
33
- @creative = Creative.find(params[:creative_id]).effective_origin
34
- unless @creative.has_permission?(Current.user, :read)
35
- render json: { error: I18n.t("collavre.creatives.errors.no_permission") }, status: :forbidden
36
- end
37
- end
38
-
39
34
  def set_snapshot
40
35
  @snapshot = @creative.comment_snapshots.find(params[:id])
41
36
  end
@@ -28,7 +28,7 @@ module Collavre
28
28
  limit = 20
29
29
 
30
30
  visible_scope = @creative.comments.visible_to(Current.user)
31
- scope = visible_scope.with_attached_images.includes(:topic, :comment_reactions, :comment_versions, :snapshot_as_result)
31
+ scope = visible_scope.with_attached_images.includes(:task, :topic, :comment_reactions, :comment_versions, :snapshot_as_result)
32
32
 
33
33
  if params[:search].present?
34
34
  words = params[:search].to_s.strip.downcase.split(/\s+/)
@@ -128,7 +128,7 @@ module Collavre
128
128
  # Fetch all visible IDs for correct read-receipt placement transparency
129
129
  # Only map read receipts to PUBLIC comments.
130
130
  # Users who read private comments will appear on the nearest preceding public comment.
131
- public_ids = @creative.comments.where(private: false).order(id: :asc).pluck(:id)
131
+ public_ids = @creative.comments.public_only.order(id: :asc).pluck(:id)
132
132
 
133
133
  pointers.each do |pointer|
134
134
  effective_id = pointer.effective_comment_id(public_ids)
@@ -169,9 +169,7 @@ module Collavre
169
169
 
170
170
  @comment = @creative.comments.build(comment_attributes)
171
171
 
172
- if @comment.topic_id.present? && !@creative.topics.where(id: @comment.topic_id).exists?
173
- render json: { error: I18n.t("collavre.comments.invalid_topic") }, status: :unprocessable_entity and return
174
- end
172
+ validate_topic_id!(@comment.topic_id) or return
175
173
 
176
174
  @comment.user = Current.user
177
175
  @comment.images.attach(image_attachments) if image_attachments.present?
@@ -193,11 +191,13 @@ module Collavre
193
191
  end
194
192
 
195
193
  def update
194
+ if github_synced_content_comment?(@comment)
195
+ render json: { error: I18n.t("collavre.comments.github_synced_readonly") }, status: :forbidden and return
196
+ end
197
+
196
198
  if @comment.user == Current.user
197
199
  safe_params = comment_params.except(:quoted_comment_id, :quoted_text)
198
- if safe_params[:topic_id].present? && !@creative.topics.where(id: safe_params[:topic_id]).exists?
199
- render json: { error: I18n.t("collavre.comments.invalid_topic") }, status: :unprocessable_entity and return
200
- end
200
+ validate_topic_id!(safe_params[:topic_id]) or return
201
201
 
202
202
  if @comment.update(safe_params)
203
203
  @comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
@@ -211,6 +211,10 @@ module Collavre
211
211
  end
212
212
 
213
213
  def destroy
214
+ if github_synced_content_comment?(@comment)
215
+ render json: { error: I18n.t("collavre.comments.github_synced_readonly") }, status: :forbidden and return
216
+ end
217
+
214
218
  # @comment is set by before_action
215
219
  is_owner = @comment.user == Current.user
216
220
  is_admin = @creative.has_permission?(Current.user, :admin)
@@ -258,17 +262,7 @@ module Collavre
258
262
  def participants
259
263
  users = [ @creative.user ].compact + @creative.all_shared_users(:feedback).map(&:user)
260
264
  users = users.uniq
261
- user_data = users.map do |u|
262
- {
263
- id: u.id,
264
- email: u.email,
265
- name: u.display_name,
266
- avatar_url: view_context.user_avatar_url(u, size: 20),
267
- default_avatar: !u.avatar.attached? && u.avatar_url.blank?,
268
- initial: u.display_name[0].upcase,
269
- ai_user: u.ai_user?
270
- }
271
- end
265
+ user_data = users.map { |u| view_context.user_json(u, email: true, ai_user: true) }
272
266
  response.headers["Cache-Control"] = "no-store"
273
267
  response.headers["Pragma"] = "no-cache"
274
268
  response.headers["Expires"] = "0"
@@ -286,7 +280,7 @@ module Collavre
286
280
  head :forbidden and return
287
281
  end
288
282
 
289
- render json: CommandMenuService.new(user: Current.user).items
283
+ render json: CommandMenuService.new(user: Current.user, creative: @creative).items
290
284
  end
291
285
 
292
286
  def download_images
@@ -345,11 +339,9 @@ module Collavre
345
339
 
346
340
  private
347
341
 
348
- def set_creative
349
- @creative = Creative.find(params[:creative_id]).effective_origin
350
- unless @creative.has_permission?(Current.user, :read)
351
- render json: { error: I18n.t("collavre.creatives.errors.no_permission") }, status: :forbidden
352
- end
342
+ def github_synced_content_comment?(comment)
343
+ return false unless comment.topic&.name == Collavre::Creative::CONTENT_TOPIC_NAME
344
+ comment.creative&.github_markdown?
353
345
  end
354
346
 
355
347
  def comment_params
@@ -359,5 +351,11 @@ module Collavre
359
351
  def current_topic_context
360
352
  params[:topic_id].presence || params.dig(:comment, :topic_id).presence
361
353
  end
354
+
355
+ def validate_topic_id!(topic_id)
356
+ return true if topic_id.blank? || @creative.topics.where(id: topic_id).exists?
357
+ render json: { error: I18n.t("collavre.comments.invalid_topic") }, status: :unprocessable_entity
358
+ false
359
+ end
362
360
  end
363
361
  end
@@ -4,12 +4,14 @@ module Collavre
4
4
  include Collavre::Concerns::Exportable
5
5
  include Collavre::Concerns::TreeManageable
6
6
  include Collavre::Concerns::Shareable
7
+ include Collavre::CreativePermissionGuard
7
8
 
8
9
  # 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
9
10
  # Removed unauthenticated access to index and show actions
10
11
  allow_unauthenticated_access only: %i[ index children export_markdown show slide_view ]
11
12
  before_action :enforce_creatives_login_policy, only: %i[ index children export_markdown show slide_view ]
12
13
  before_action :set_creative, only: %i[ show edit update destroy parent_suggestions slide_view request_permission unconvert contexts update_contexts update_metadata archive unarchive trigger_action ]
14
+ before_action :require_creative_write!, only: %i[archive unarchive]
13
15
 
14
16
  def index
15
17
  respond_to do |format|
@@ -195,6 +197,10 @@ module Collavre
195
197
  end
196
198
 
197
199
  def parent_suggestions
200
+ unless @creative.has_permission?(Current.user, :read)
201
+ render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
202
+ end
203
+
198
204
  suggestions = ::GeminiParentRecommender.new.recommend(@creative)
199
205
  render json: suggestions
200
206
  end
@@ -276,13 +282,17 @@ module Collavre
276
282
  end
277
283
 
278
284
  def contexts
285
+ unless @creative.has_permission?(Current.user, :read)
286
+ render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
287
+ end
288
+
279
289
  creative = @creative.effective_origin(Set.new)
280
290
  own_ids = creative.context_ids - [ creative.id ]
281
291
  inherited_ids = (creative.effective_context_ids - own_ids - [ creative.id ]).uniq
282
292
  own_creatives = Creative.where(id: own_ids).index_by(&:id)
283
293
  inherited_creatives = Creative.where(id: inherited_ids).index_by(&:id)
284
294
 
285
- disabled_ids = Array(creative.data&.dig("disabled_context_ids"))
295
+ disabled_ids = creative.effective_disabled_context_ids
286
296
 
287
297
  own = own_ids.filter_map do |cid|
288
298
  c = own_creatives[cid]
@@ -314,8 +324,13 @@ module Collavre
314
324
 
315
325
  current_data = (creative.data || {}).dup
316
326
  current_data["context_ids"] = Array(params[:context_ids]).map(&:to_i) if params.key?(:context_ids)
317
- current_data["disabled_context_ids"] = Array(params[:disabled_context_ids]).map(&:to_i) if params.key?(:disabled_context_ids)
318
- current_data.delete("disabled_context_ids") if current_data["disabled_context_ids"]&.empty?
327
+ if params.key?(:disabled_context_ids)
328
+ requested_disabled = Array(params[:disabled_context_ids]).map(&:to_i)
329
+ parent_disabled = creative.parent&.effective_disabled_context_ids || []
330
+ inherited_only = parent_disabled - creative.disabled_context_ids
331
+ current_data["disabled_context_ids"] = requested_disabled - inherited_only
332
+ current_data.delete("disabled_context_ids") if current_data["disabled_context_ids"].empty?
333
+ end
319
334
  if params.key?(:disabled_self_context)
320
335
  if ActiveModel::Type::Boolean.new.cast(params[:disabled_self_context])
321
336
  current_data["disabled_self_context"] = true
@@ -440,19 +455,11 @@ module Collavre
440
455
  end
441
456
 
442
457
  def archive
443
- unless @creative.has_permission?(Current.user, :write) || @creative.user == Current.user
444
- render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
445
- end
446
-
447
458
  @creative.archive!
448
459
  head :ok
449
460
  end
450
461
 
451
462
  def unarchive
452
- unless @creative.has_permission?(Current.user, :write) || @creative.user == Current.user
453
- render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
454
- end
455
-
456
463
  @creative.unarchive!
457
464
  head :ok
458
465
  end
@@ -487,6 +494,10 @@ module Collavre
487
494
  @creative = Creative.find(params[:id])
488
495
  end
489
496
 
497
+ def creative_permission_denied_message
498
+ t("collavre.creatives.errors.no_permission")
499
+ end
500
+
490
501
  def creative_params
491
502
  params.require(:creative).permit(:description, :progress, :parent_id, :sequence, :origin_id)
492
503
  end
@@ -1,5 +1,7 @@
1
1
  module Collavre
2
2
  class EmailsController < ApplicationController
3
+ before_action :require_system_admin!
4
+
3
5
  def index
4
6
  @emails = Email.order(created_at: :desc).limit(50)
5
7
  end
@@ -33,7 +33,7 @@ module Collavre
33
33
  # Ensure app calendar exists if the granted scope allows creating an app calendar
34
34
  begin
35
35
  ::GoogleCalendarService.new(user: user).ensure_app_calendar!
36
- rescue => e
36
+ rescue StandardError => e
37
37
  Rails.logger.error("Post-login calendar setup failed: #{e.message}")
38
38
  end
39
39
 
@@ -58,7 +58,7 @@ module Collavre
58
58
  private
59
59
 
60
60
  def set_inbox_item
61
- @inbox_item = InboxItem.find(params[:id])
61
+ @inbox_item = InboxItem.where(owner: Current.user).find(params[:id])
62
62
  end
63
63
  end
64
64
  end
@@ -5,6 +5,9 @@ module Collavre
5
5
 
6
6
  def create
7
7
  creative = Creative.find(params[:creative_id]).effective_origin
8
+ unless creative.has_permission?(Current.user, :admin)
9
+ return head :forbidden
10
+ end
8
11
  permission = params[:permission] || :read
9
12
  invitation = Invitation.create!(inviter: Current.user,
10
13
  creative: creative,
@@ -10,7 +10,7 @@ module Collavre
10
10
  return head :forbidden
11
11
  end
12
12
 
13
- unless %w[running pending queued].include?(task.status)
13
+ unless %w[running pending queued pending_approval].include?(task.status)
14
14
  return head :unprocessable_entity
15
15
  end
16
16