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
@@ -0,0 +1,189 @@
1
+ /**
2
+ * ReviewQuotesStore — Pure state management for review quote chips.
3
+ *
4
+ * Responsible for:
5
+ * - Adding / removing / updating quotes
6
+ * - Tracking the active (editing) quote
7
+ * - Building markdown content for submission
8
+ * - Backup / restore for send-failure rollback
9
+ *
10
+ * Does NOT touch the DOM — the controller handles all rendering.
11
+ */
12
+ let _nextId = 1
13
+
14
+ export default class ReviewQuotesStore {
15
+ constructor() {
16
+ this._quotes = []
17
+ this._activeId = null
18
+ this._backup = null
19
+ }
20
+
21
+ // --- Accessors ---
22
+
23
+ get quotes() {
24
+ return this._quotes
25
+ }
26
+
27
+ get activeId() {
28
+ return this._activeId
29
+ }
30
+
31
+ set activeId(id) {
32
+ this._activeId = id
33
+ }
34
+
35
+ get activeQuote() {
36
+ if (!this._activeId) return null
37
+ return this._quotes.find(q => q.id === this._activeId) || null
38
+ }
39
+
40
+ get count() {
41
+ return this._quotes.length
42
+ }
43
+
44
+ get isEmpty() {
45
+ return this._quotes.length === 0
46
+ }
47
+
48
+ get hasActive() {
49
+ return this._activeId !== null
50
+ }
51
+
52
+ // --- Mutations ---
53
+
54
+ add(commentId, text) {
55
+ const quote = {
56
+ id: _nextId++,
57
+ commentId,
58
+ text,
59
+ type: 'review', // 'review' | 'question'
60
+ feedback: '',
61
+ }
62
+ this._quotes.push(quote)
63
+ this._activeId = quote.id
64
+ return quote
65
+ }
66
+
67
+ remove(quoteId) {
68
+ const idx = this._quotes.findIndex(q => q.id === quoteId)
69
+ if (idx === -1) return null
70
+ const [removed] = this._quotes.splice(idx, 1)
71
+ if (this._activeId === quoteId) {
72
+ this._activeId = null
73
+ }
74
+ return removed
75
+ }
76
+
77
+ setFeedback(quoteId, feedback) {
78
+ const quote = this._quotes.find(q => q.id === quoteId)
79
+ if (quote) quote.feedback = feedback.trim()
80
+ }
81
+
82
+ saveActiveFeedback(textareaValue) {
83
+ if (!this._activeId) return
84
+ const quote = this._quotes.find(q => q.id === this._activeId)
85
+ if (quote) quote.feedback = textareaValue.trim()
86
+ }
87
+
88
+ commitActive(textareaValue) {
89
+ this.saveActiveFeedback(textareaValue)
90
+ this._activeId = null
91
+ }
92
+
93
+ toggleType(quoteId) {
94
+ const quote = this._quotes.find(q => q.id === quoteId)
95
+ if (!quote) return null
96
+ quote.type = quote.type === 'review' ? 'question' : 'review'
97
+ return quote
98
+ }
99
+
100
+ activate(quoteId) {
101
+ const quote = this._quotes.find(q => q.id === quoteId)
102
+ if (!quote) return null
103
+ this._activeId = quoteId
104
+ return quote
105
+ }
106
+
107
+ clear() {
108
+ this._quotes = []
109
+ this._activeId = null
110
+ this._backup = null
111
+ }
112
+
113
+ // --- Backup / Restore (for send-failure rollback) ---
114
+
115
+ backup(textareaValue) {
116
+ this._backup = {
117
+ quotes: JSON.parse(JSON.stringify(this._quotes)),
118
+ textareaValue,
119
+ }
120
+ }
121
+
122
+ restore() {
123
+ if (!this._backup) return null
124
+ this._quotes = this._backup.quotes
125
+ const textareaValue = this._backup.textareaValue
126
+ this._backup = null
127
+ return textareaValue
128
+ }
129
+
130
+ hasBackup() {
131
+ return this._backup !== null
132
+ }
133
+
134
+ // --- Markdown Builder ---
135
+
136
+ buildContent(summaryText) {
137
+ if (this._quotes.length === 0) return summaryText
138
+
139
+ const parts = []
140
+
141
+ this._quotes.forEach((q, i) => {
142
+ if (i > 0) parts.push('') // Blank line between quotes to prevent blockquote merging
143
+ const prefix = q.type === 'question' ? '> ❓ ' : '> '
144
+ const quoted = q.text.split('\n').map((line, j) => {
145
+ return j === 0 ? `${prefix}${line}` : `> ${line}`
146
+ }).join('\n')
147
+ parts.push(quoted)
148
+ if (q.feedback) {
149
+ parts.push('')
150
+ parts.push(q.feedback)
151
+ }
152
+ })
153
+
154
+ const summary = summaryText.trim()
155
+ if (summary) {
156
+ parts.push('')
157
+ parts.push('---')
158
+ parts.push(summary)
159
+ }
160
+
161
+ return parts.join('\n')
162
+ }
163
+
164
+ buildQuestionContent(quote) {
165
+ const prefix = '> ❓ '
166
+ const quoted = quote.text.split('\n').map((line, i) => {
167
+ return i === 0 ? `${prefix}${line}` : `> ${line}`
168
+ }).join('\n')
169
+ const parts = [quoted]
170
+ if (quote.feedback) {
171
+ parts.push('')
172
+ parts.push(quote.feedback)
173
+ }
174
+ return parts.join('\n')
175
+ }
176
+
177
+ // --- Query ---
178
+
179
+ /** Button state: 'add' | 'send-review' | 'send-question' | 'normal' */
180
+ get buttonState() {
181
+ if (this._quotes.length === 0) return 'normal'
182
+ if (this._activeId) {
183
+ const active = this.activeQuote
184
+ if (active && active.type === 'question') return 'send-question'
185
+ return 'add'
186
+ }
187
+ return 'send-review'
188
+ }
189
+ }
@@ -7,6 +7,7 @@ export default class extends Controller {
7
7
  connect() {
8
8
  this.topics = []
9
9
  this.canManageTopics = false
10
+ this.canCreateTopic = false
10
11
  this.subscribedCreativeId = null
11
12
  this.topicsSubscription = null
12
13
  // Initial load if creativeId is available (e.g. from dataset if set server-side)
@@ -15,11 +16,14 @@ export default class extends Controller {
15
16
  this.subscribe()
16
17
  }
17
18
  this.handleNewMessage = this.handleNewMessage.bind(this)
19
+ this.handleTopicMoved = this.handleTopicMoved.bind(this)
18
20
  window.addEventListener('comments--topics:new-message', this.handleNewMessage)
21
+ window.addEventListener('collavre:topic-moved', this.handleTopicMoved)
19
22
  }
20
23
 
21
24
  disconnect() {
22
25
  window.removeEventListener('comments--topics:new-message', this.handleNewMessage)
26
+ window.removeEventListener('collavre:topic-moved', this.handleTopicMoved)
23
27
  this.unsubscribe()
24
28
  }
25
29
 
@@ -59,9 +63,11 @@ export default class extends Controller {
59
63
  const data = await response.json()
60
64
  const topics = Array.isArray(data) ? data : data.topics
61
65
  const canManage = Array.isArray(data) ? false : data.can_manage
66
+ const canCreateTopic = Array.isArray(data) ? false : (data.can_create_topic ?? canManage)
62
67
  this.topics = topics
63
68
  this.canManageTopics = canManage
64
- this.renderTopics(this.topics, this.canManageTopics)
69
+ this.canCreateTopic = canCreateTopic
70
+ this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
65
71
  this.restoreSelection()
66
72
  }
67
73
  } catch (e) {
@@ -85,7 +91,7 @@ export default class extends Controller {
85
91
  }
86
92
  }
87
93
 
88
- renderTopics(topics, canManage = false) {
94
+ renderTopics(topics, canManage = false, canCreateTopic = canManage) {
89
95
  const dragActions = canManage
90
96
  ? 'dragstart->comments--topics#handleTopicDragStart dragend->comments--topics#handleTopicDragEnd'
91
97
  : ''
@@ -114,8 +120,8 @@ export default class extends Controller {
114
120
  html += `</span>`
115
121
  })
116
122
 
117
- // Add create button container
118
- if (canManage) {
123
+ // Add create button container (write permission is sufficient for topic creation)
124
+ if (canCreateTopic) {
119
125
  html += `<span class="topic-creation-container" data-comments--topics-target="creationContainer">
120
126
  <button class="add-topic-btn" data-action="click->comments--topics#showInput">+</button>
121
127
  </span>`
@@ -169,6 +175,11 @@ export default class extends Controller {
169
175
 
170
176
  this.draggingTopicId = topicId
171
177
  event.dataTransfer.setData('application/x-topic-id', topicId)
178
+ // Include topic move data so creative tree rows can accept this drop
179
+ event.dataTransfer.setData('application/x-topic-move', JSON.stringify({
180
+ topicId,
181
+ sourceCreativeId: this.creativeId
182
+ }))
172
183
  event.dataTransfer.effectAllowed = 'move'
173
184
 
174
185
  requestAnimationFrame(() => {
@@ -250,7 +261,7 @@ export default class extends Controller {
250
261
 
251
262
  // Update local state and UI immediately
252
263
  this.topics = topics
253
- this.renderTopics(topics, this.canManageTopics)
264
+ this.renderTopics(topics, this.canManageTopics, this.canCreateTopic)
254
265
  this.restoreSelection()
255
266
 
256
267
  // Send to server
@@ -446,7 +457,7 @@ export default class extends Controller {
446
457
  if (index !== -1) {
447
458
  this.topics[index] = updatedTopic
448
459
  }
449
- this.renderTopics(this.topics, this.canManageTopics)
460
+ this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
450
461
  this.restoreSelection()
451
462
  } else {
452
463
  alert("Failed to update topic")
@@ -483,6 +494,19 @@ export default class extends Controller {
483
494
  }
484
495
  }
485
496
 
497
+ handleTopicMoved(event) {
498
+ const { sourceCreativeId, topicId } = event.detail
499
+ // Reload topics if the moved topic belonged to the currently viewed creative
500
+ if (String(sourceCreativeId) === String(this.creativeId)) {
501
+ // If we were viewing the moved topic, switch to Main
502
+ if (String(this.currentTopicId) === String(topicId)) {
503
+ this.currentTopicId = ""
504
+ this.dispatch("change", { detail: { topicId: "" } })
505
+ }
506
+ this.loadTopics()
507
+ }
508
+ }
509
+
486
510
  handleNewMessage(event) {
487
511
  const topicId = event.detail.topicId
488
512
  if (!topicId) return
@@ -601,7 +625,7 @@ export default class extends Controller {
601
625
  if (exists) return
602
626
 
603
627
  this.topics = [...topics, data.topic]
604
- this.renderTopics(this.topics, this.canManageTopics)
628
+ this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
605
629
  this.restoreSelection()
606
630
  }
607
631
 
@@ -622,7 +646,7 @@ export default class extends Controller {
622
646
  })
623
647
 
624
648
  this.topics = reorderedTopics
625
- this.renderTopics(this.topics, this.canManageTopics)
649
+ this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
626
650
  this.restoreSelection()
627
651
  }
628
652
 
@@ -632,7 +656,7 @@ export default class extends Controller {
632
656
  if (index === -1) return
633
657
 
634
658
  this.topics[index] = updatedTopic
635
- this.renderTopics(this.topics, this.canManageTopics)
659
+ this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
636
660
  this.restoreSelection()
637
661
  }
638
662
 
@@ -649,7 +673,7 @@ export default class extends Controller {
649
673
  this.dispatch("change", { detail: { topicId: "" } })
650
674
  }
651
675
 
652
- this.renderTopics(this.topics, this.canManageTopics)
676
+ this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
653
677
  this.restoreSelection()
654
678
  }
655
679
  }
@@ -14,15 +14,20 @@ import CommentsFormController from "./comments/form_controller"
14
14
  import CommentsPresenceController from "./comments/presence_controller"
15
15
  import CommentsMentionMenuController from "./comments/mention_menu_controller"
16
16
  import CommentsTopicsController from "./comments/topics_controller"
17
+ import CommentsContextsController from "./comments/contexts_controller"
17
18
  import CommentsPopupController from "./comments/popup_controller"
18
19
  import ClickTargetController from "./click_target_controller"
19
20
  import TabsController from "./tabs_controller"
20
21
  import LinkCreativeController from "./link_creative_controller"
22
+ import TopicSearchController from "./topic_search_controller"
21
23
  import CommonPopupController from "./common_popup_controller"
22
24
  import CommentController from "./comment_controller"
23
25
  import ReactionPickerController from "./reaction_picker_controller"
24
26
  import ShareInviteController from "./share_invite_controller"
25
27
  import ShareUserSearchController from "./share_user_search_controller"
28
+ import CommentVersionController from "./comment_version_controller"
29
+ import OrgChartController from "./org_chart_controller"
30
+ import ShareModalController from "./share_modal_controller"
26
31
 
27
32
  // Export all controllers
28
33
  export {
@@ -44,11 +49,15 @@ export {
44
49
  ClickTargetController,
45
50
  TabsController,
46
51
  LinkCreativeController,
52
+ TopicSearchController,
47
53
  CommonPopupController,
48
54
  CommentController,
49
55
  ReactionPickerController,
50
56
  ShareInviteController,
51
- ShareUserSearchController
57
+ ShareUserSearchController,
58
+ CommentVersionController,
59
+ OrgChartController,
60
+ ShareModalController
52
61
  }
53
62
 
54
63
  // Registration function for use with a Stimulus application
@@ -67,13 +76,18 @@ export function registerControllers(application) {
67
76
  application.register("comments--presence", CommentsPresenceController)
68
77
  application.register("comments--mention-menu", CommentsMentionMenuController)
69
78
  application.register("comments--topics", CommentsTopicsController)
79
+ application.register("comments--contexts", CommentsContextsController)
70
80
  application.register("comments--popup", CommentsPopupController)
71
81
  application.register("click-target", ClickTargetController)
72
82
  application.register("tabs", TabsController)
73
83
  application.register("link-creative", LinkCreativeController)
84
+ application.register("topic-search", TopicSearchController)
74
85
  application.register("common-popup", CommonPopupController)
75
86
  application.register("comment", CommentController)
76
87
  application.register("reaction-picker", ReactionPickerController)
77
88
  application.register("share-invite", ShareInviteController)
78
89
  application.register("share-user-search", ShareUserSearchController)
90
+ application.register("comment-version", CommentVersionController)
91
+ application.register("org-chart", OrgChartController)
92
+ application.register("share-modal", ShareModalController)
79
93
  }
@@ -0,0 +1,46 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = [ "toggleAllBtn" ]
5
+
6
+ toggleAll() {
7
+ const details = this.element.querySelectorAll("details.org-chart-group")
8
+ const allOpen = Array.from(details).every(d => d.open)
9
+
10
+ details.forEach(d => d.open = !allOpen)
11
+
12
+ if (this.hasToggleAllBtnTarget) {
13
+ const btn = this.toggleAllBtnTarget
14
+ btn.textContent = allOpen
15
+ ? btn.dataset.expandText || btn.textContent
16
+ : btn.dataset.collapseText || btn.textContent
17
+ }
18
+ }
19
+
20
+ updatePermission(event) {
21
+ const select = event.target
22
+ const url = select.dataset.updateUrl
23
+ const permission = select.value
24
+ const originalClass = select.className
25
+
26
+ // Update visual class immediately
27
+ select.className = select.className.replace(/org-chart-permission-\w+/g, "")
28
+ select.classList.add("org-chart-permission-select", `org-chart-permission-${permission}`)
29
+
30
+ fetch(url, {
31
+ method: "PATCH",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ "X-CSRF-Token": document.querySelector("meta[name='csrf-token']")?.content,
35
+ "Accept": "application/json"
36
+ },
37
+ body: JSON.stringify({ permission })
38
+ }).then(response => {
39
+ if (!response.ok) throw new Error("Failed")
40
+ }).catch(() => {
41
+ select.className = originalClass
42
+ location.reload()
43
+ })
44
+ }
45
+
46
+ }