collavre 0.22.0 → 0.23.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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/assets/stylesheets/collavre/actiontext.css +251 -90
  4. data/app/assets/stylesheets/collavre/code_highlight.css +7 -201
  5. data/app/assets/stylesheets/collavre/comments_popup.css +118 -61
  6. data/app/assets/stylesheets/collavre/creatives.css +11 -2
  7. data/app/assets/stylesheets/collavre/modal_dialog.css +32 -0
  8. data/app/assets/stylesheets/collavre/tables.css +91 -0
  9. data/app/channels/collavre/inbox_badge_channel.rb +30 -0
  10. data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +224 -0
  11. data/app/controllers/collavre/api/v1/mobile/base_controller.rb +95 -0
  12. data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +31 -0
  13. data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +25 -0
  14. data/app/controllers/collavre/creatives_controller.rb +16 -5
  15. data/app/controllers/collavre/tasks_controller.rb +13 -4
  16. data/app/controllers/collavre/topics_controller.rb +49 -1
  17. data/app/controllers/collavre/typo_corrections_controller.rb +39 -0
  18. data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +16 -1
  19. data/app/controllers/concerns/collavre/users_controller/registration.rb +41 -1
  20. data/app/helpers/collavre/application_helper.rb +1 -0
  21. data/app/javascript/collavre.js +2 -0
  22. data/app/javascript/components/ImageResizer.jsx +9 -3
  23. data/app/javascript/components/InlineLexicalEditor.jsx +155 -38
  24. data/app/javascript/components/creative_tree_row.js +20 -3
  25. data/app/javascript/components/plugins/list_tab_indent_plugin.jsx +16 -0
  26. data/app/javascript/components/plugins/table_hover_actions_plugin.jsx +405 -0
  27. data/app/javascript/controllers/__tests__/inbox_badge_controller.test.js +73 -0
  28. data/app/javascript/controllers/comment_controller.js +5 -4
  29. data/app/javascript/controllers/comment_version_controller.js +2 -1
  30. data/app/javascript/controllers/comments/__tests__/form_controller_double_submit.test.js +159 -0
  31. data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +3 -2
  32. data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +94 -0
  33. data/app/javascript/controllers/comments/form_controller.js +21 -5
  34. data/app/javascript/controllers/comments/list_controller.js +18 -17
  35. data/app/javascript/controllers/comments/presence_controller.js +2 -1
  36. data/app/javascript/controllers/comments/topics_controller.js +14 -8
  37. data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +150 -0
  38. data/app/javascript/controllers/creatives/import_controller.js +2 -1
  39. data/app/javascript/controllers/creatives/select_mode_controller.js +2 -1
  40. data/app/javascript/controllers/creatives/tree_controller.js +142 -1
  41. data/app/javascript/controllers/image_lightbox_controller.js +2 -1
  42. data/app/javascript/controllers/inbox_badge_controller.js +33 -0
  43. data/app/javascript/controllers/index.js +4 -1
  44. data/app/javascript/controllers/share_modal_controller.js +4 -3
  45. data/app/javascript/controllers/topic_search_controller.js +2 -1
  46. data/app/javascript/creatives/drag_drop/event_handlers.js +14 -5
  47. data/app/javascript/creatives/topic_move_members_popup.js +156 -0
  48. data/app/javascript/creatives/tree_renderer.js +11 -0
  49. data/app/javascript/lib/__tests__/turbo_confirm.test.js +81 -0
  50. data/app/javascript/lib/__tests__/typo_correction.test.js +192 -0
  51. data/app/javascript/lib/api/__tests__/api_error.test.js +96 -0
  52. data/app/javascript/lib/api/__tests__/queue_manager.test.js +88 -1
  53. data/app/javascript/lib/api/api_error.js +108 -0
  54. data/app/javascript/lib/api/queue_manager.js +38 -4
  55. data/app/javascript/lib/common_popup.js +18 -5
  56. data/app/javascript/lib/editor/__tests__/code_edit_view_token_parity.test.js +121 -0
  57. data/app/javascript/lib/editor/__tests__/code_language_roundtrip.test.js +152 -0
  58. data/app/javascript/lib/editor/__tests__/code_languages.test.js +93 -0
  59. data/app/javascript/lib/editor/code_languages.js +173 -0
  60. data/app/javascript/lib/editor/code_token_theme.js +41 -0
  61. data/app/javascript/lib/lexical/__tests__/image_focus.test.js +139 -0
  62. data/app/javascript/lib/lexical/__tests__/list_tab_indent.test.js +633 -0
  63. data/app/javascript/lib/lexical/__tests__/markdown_serialize.test.js +627 -0
  64. data/app/javascript/lib/lexical/__tests__/minimize_html.test.js +20 -1
  65. data/app/javascript/lib/lexical/__tests__/selection_boundary.test.js +88 -0
  66. data/app/javascript/lib/lexical/__tests__/table_transformer.test.js +163 -0
  67. data/app/javascript/lib/lexical/__tests__/trailing_paragraph.test.js +104 -0
  68. data/app/javascript/lib/lexical/list_tab_indent.js +210 -0
  69. data/app/javascript/lib/lexical/markdown_serialize.js +320 -0
  70. data/app/javascript/lib/lexical/selection_boundary.js +58 -0
  71. data/app/javascript/lib/lexical/table_transformer.js +182 -0
  72. data/app/javascript/lib/lexical/trailing_paragraph.js +29 -0
  73. data/app/javascript/lib/turbo_confirm.js +46 -0
  74. data/app/javascript/lib/typo_correction.js +146 -0
  75. data/app/javascript/lib/utils/__tests__/confirm_dialog.test.js +88 -0
  76. data/app/javascript/lib/utils/__tests__/dialog.test.js +92 -0
  77. data/app/javascript/lib/utils/__tests__/markdown.test.js +153 -0
  78. data/app/javascript/lib/utils/__tests__/sanitize_description.test.js +68 -0
  79. data/app/javascript/lib/utils/__tests__/table_download.test.js +93 -0
  80. data/app/javascript/lib/utils/confirm_dialog.js +10 -0
  81. data/app/javascript/lib/utils/dialog.js +300 -0
  82. data/app/javascript/lib/utils/markdown.js +154 -67
  83. data/app/javascript/lib/utils/sanitize_description.js +31 -0
  84. data/app/javascript/lib/utils/table_download.js +15 -0
  85. data/app/javascript/modules/__tests__/typo_corrector.test.js +365 -0
  86. data/app/javascript/modules/creative_row_editor.js +110 -70
  87. data/app/javascript/modules/export_to_markdown.js +2 -1
  88. data/app/javascript/modules/lexical_inline_editor.jsx +2 -1
  89. data/app/javascript/modules/slide_view.js +11 -2
  90. data/app/javascript/modules/typo_corrector.js +534 -0
  91. data/app/jobs/collavre/ai_agent_job.rb +7 -4
  92. data/app/jobs/collavre/compress_job.rb +6 -2
  93. data/app/models/collavre/comment/broadcastable.rb +46 -7
  94. data/app/models/collavre/comment/notifiable.rb +14 -4
  95. data/app/models/collavre/comment.rb +79 -31
  96. data/app/models/collavre/creative/describable.rb +89 -10
  97. data/app/models/collavre/task.rb +15 -0
  98. data/app/models/collavre/user.rb +57 -1
  99. data/app/services/collavre/ai_client.rb +28 -10
  100. data/app/services/collavre/auto_theme_generator.rb +1 -1
  101. data/app/services/collavre/creatives/index_query.rb +85 -16
  102. data/app/services/collavre/creatives/tree_builder.rb +2 -1
  103. data/app/services/collavre/gemini_parent_recommender.rb +1 -1
  104. data/app/services/collavre/inbox_reply_service.rb +5 -0
  105. data/app/services/collavre/markdown_converter.rb +13 -3
  106. data/app/services/collavre/mobile/event_summarizer.rb +40 -0
  107. data/app/services/collavre/orchestration/agent_orchestrator.rb +33 -7
  108. data/app/services/collavre/orchestration/arbiter.rb +16 -0
  109. data/app/services/collavre/orchestration/matcher.rb +79 -4
  110. data/app/services/collavre/orchestration/policy_resolver.rb +4 -3
  111. data/app/services/collavre/orchestration/stuck_detector.rb +141 -34
  112. data/app/services/collavre/tools/creative_batch_service.rb +3 -2
  113. data/app/services/collavre/tools/creative_create_service.rb +8 -8
  114. data/app/services/collavre/tools/creative_update_service.rb +23 -8
  115. data/app/services/collavre/typo_corrector.rb +188 -0
  116. data/app/views/collavre/comments/_comment.html.erb +5 -0
  117. data/app/views/collavre/comments/_comments_popup.html.erb +14 -1
  118. data/app/views/collavre/creatives/_inline_edit_form.html.erb +1 -0
  119. data/app/views/collavre/creatives/_topic_move_members_modal.html.erb +42 -0
  120. data/app/views/collavre/creatives/index.html.erb +14 -1
  121. data/app/views/collavre/creatives/slide_view.html.erb +1 -1
  122. data/app/views/collavre/users/show.html.erb +3 -0
  123. data/app/views/collavre/users/typo_correction.html.erb +50 -0
  124. data/app/views/layouts/collavre/slide.html.erb +1 -0
  125. data/config/locales/comments.en.yml +15 -0
  126. data/config/locales/comments.ko.yml +15 -0
  127. data/config/locales/integrations.en.yml +1 -1
  128. data/config/locales/integrations.ko.yml +1 -1
  129. data/config/locales/mobile.en.yml +16 -0
  130. data/config/locales/mobile.ko.yml +16 -0
  131. data/config/locales/orchestration.en.yml +1 -0
  132. data/config/locales/orchestration.ko.yml +1 -0
  133. data/config/locales/users.en.yml +15 -0
  134. data/config/locales/users.ko.yml +15 -0
  135. data/config/routes.rb +13 -0
  136. data/db/migrate/20260612000000_add_topic_concurrency_defer_to_comments.rb +38 -0
  137. data/db/migrate/20260617090000_add_typo_correction_settings_to_users.rb +18 -0
  138. data/db/seeds.rb +51 -0
  139. data/lib/collavre/version.rb +1 -1
  140. data/lib/generators/collavre/install/install_generator.rb +1 -0
  141. metadata +55 -2
  142. data/app/services/collavre/tools/description_normalizable.rb +0 -16
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ *
4
+ * Regression for "an external image steals editing focus": clicking into the
5
+ * text to edit while an image is present used to wipe the caret. ImageResizer
6
+ * registered a CLICK_COMMAND handler that called the node-selection hook's
7
+ * setSelected(false) on any click outside the image. That hook, when the live
8
+ * selection is NOT already a NodeSelection (i.e. it is the RangeSelection caret
9
+ * the click just placed), synthesizes a fresh empty NodeSelection and replaces
10
+ * the caret with it — so the editor lost its caret and became uneditable the
11
+ * instant you clicked to edit. The fix uses clearSelection(), which only acts on
12
+ * an existing NodeSelection and leaves a RangeSelection (the caret) intact.
13
+ *
14
+ * ImageResizer is a .jsx React component that can't be imported under this
15
+ * native-ESM jest config, so this test reconstructs the two deselect paths and
16
+ * the exact CLICK_COMMAND handler logic on a headless editor.
17
+ */
18
+ import {
19
+ createEditor,
20
+ $getRoot,
21
+ $getSelection,
22
+ $isRangeSelection,
23
+ $isNodeSelection,
24
+ $createNodeSelection,
25
+ $setSelection,
26
+ $createParagraphNode,
27
+ $createTextNode,
28
+ $applyNodeReplacement,
29
+ DecoratorNode,
30
+ CLICK_COMMAND,
31
+ COMMAND_PRIORITY_LOW,
32
+ } from "lexical"
33
+
34
+ class TestImageNode extends DecoratorNode {
35
+ __src
36
+ static getType() { return "image" }
37
+ static clone(n) { return new TestImageNode(n.__src, n.__key) }
38
+ static importJSON() { return $createImage("") }
39
+ constructor(src = "", key) { super(key); this.__src = src }
40
+ createDOM() { return document.createElement("span") }
41
+ updateDOM() { return false }
42
+ decorate() { return null }
43
+ }
44
+ function $createImage(src) { return $applyNodeReplacement(new TestImageNode(src)) }
45
+
46
+ // Build: [image][paragraph "hello"] with the caret placed inside the text,
47
+ // i.e. the state right after a user clicks into the paragraph to edit.
48
+ function setup() {
49
+ const editor = createEditor({ namespace: "t", nodes: [TestImageNode], onError: (e) => { throw e } })
50
+ // jsdom: createEditor needs a root element to dispatch DOM-bound commands.
51
+ const el = document.createElement("div")
52
+ el.contentEditable = "true"
53
+ document.body.appendChild(el)
54
+ editor.setRootElement(el)
55
+ let imageKey
56
+ editor.update(() => {
57
+ const root = $getRoot()
58
+ root.clear()
59
+ root.append($createImage("https://lipcoding.kr/x.png"))
60
+ const p = $createParagraphNode()
61
+ p.append($createTextNode("hello"))
62
+ root.append(p)
63
+ imageKey = root.getFirstChild().getKey()
64
+ }, { discrete: true })
65
+ editor.update(() => {
66
+ $getRoot().getLastChild().getFirstChild().select(2, 2)
67
+ }, { discrete: true })
68
+ return { editor, imageKey }
69
+ }
70
+
71
+ function selKind(editor) {
72
+ let kind = "none"
73
+ editor.getEditorState().read(() => {
74
+ const s = $getSelection()
75
+ if ($isRangeSelection(s)) kind = "range"
76
+ else if ($isNodeSelection(s)) kind = "node"
77
+ })
78
+ return kind
79
+ }
80
+
81
+ // Mirror of the hook's setSelected(false): the OLD, destructive deselect path.
82
+ function setSelectedFalse(editor, key) {
83
+ editor.update(() => {
84
+ let selection = $getSelection()
85
+ if (!$isNodeSelection(selection)) {
86
+ selection = $createNodeSelection()
87
+ $setSelection(selection)
88
+ }
89
+ if ($isNodeSelection(selection)) selection.delete(key)
90
+ }, { discrete: true })
91
+ }
92
+
93
+ // Mirror of the hook's clearSelection(): the safe deselect path the fix uses.
94
+ function clearSelection(editor) {
95
+ editor.update(() => {
96
+ const selection = $getSelection()
97
+ if ($isNodeSelection(selection)) selection.clear()
98
+ }, { discrete: true })
99
+ }
100
+
101
+ describe("image editing focus", () => {
102
+ test("the old setSelected(false) deselect destroys the caret (documents the bug)", () => {
103
+ const { editor, imageKey } = setup()
104
+ expect(selKind(editor)).toBe("range")
105
+ setSelectedFalse(editor, imageKey)
106
+ expect(selKind(editor)).not.toBe("range") // caret lost — the reported bug
107
+ })
108
+
109
+ test("clearSelection() leaves the caret intact", () => {
110
+ const { editor } = setup()
111
+ expect(selKind(editor)).toBe("range")
112
+ clearSelection(editor)
113
+ expect(selKind(editor)).toBe("range")
114
+ })
115
+
116
+ test("CLICK_COMMAND outside the image keeps the caret (fixed handler)", () => {
117
+ const { editor } = setup()
118
+
119
+ // Stand-in for ImageResizer's container ref; the click target is text
120
+ // outside it, exactly like clicking into the paragraph to edit.
121
+ const container = document.createElement("span")
122
+ const outsideTarget = document.createElement("span")
123
+
124
+ editor.registerCommand(
125
+ CLICK_COMMAND,
126
+ (event) => {
127
+ if (container && !container.contains(event.target)) {
128
+ clearSelection(editor) // fixed path (was setSelected(false))
129
+ }
130
+ return false
131
+ },
132
+ COMMAND_PRIORITY_LOW
133
+ )
134
+
135
+ expect(selKind(editor)).toBe("range")
136
+ editor.dispatchCommand(CLICK_COMMAND, { target: outsideTarget })
137
+ expect(selKind(editor)).toBe("range") // caret survives the click
138
+ })
139
+ })