collavre 0.20.3 → 0.22.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 (163) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/actiontext.css +92 -2
  3. data/app/assets/stylesheets/collavre/code_highlight.css +144 -26
  4. data/app/assets/stylesheets/collavre/comments_popup.css +133 -2
  5. data/app/assets/stylesheets/collavre/landing.css +507 -0
  6. data/app/assets/stylesheets/collavre/popup.css +148 -0
  7. data/app/channels/collavre/agent_channel.rb +205 -0
  8. data/app/channels/collavre/comments_presence_channel.rb +7 -0
  9. data/app/controllers/collavre/admin/integrations_controller.rb +93 -0
  10. data/app/controllers/collavre/admin/settings_controller.rb +22 -17
  11. data/app/controllers/collavre/api/v1/agents_controller.rb +777 -0
  12. data/app/controllers/collavre/api/v1/base_controller.rb +46 -0
  13. data/app/controllers/collavre/application_controller.rb +27 -0
  14. data/app/controllers/collavre/attachments_controller.rb +30 -2
  15. data/app/controllers/collavre/channels_controller.rb +23 -0
  16. data/app/controllers/collavre/comments_controller.rb +1 -1
  17. data/app/controllers/collavre/creatives/attachments_controller.rb +79 -0
  18. data/app/controllers/collavre/creatives_controller.rb +141 -7
  19. data/app/controllers/collavre/landing_controller.rb +8 -0
  20. data/app/controllers/collavre/public_assets_controller.rb +24 -0
  21. data/app/controllers/collavre/tasks_controller.rb +12 -4
  22. data/app/controllers/collavre/topics_controller.rb +36 -30
  23. data/app/controllers/concerns/collavre/comments/approval_actions.rb +57 -14
  24. data/app/helpers/collavre/comments_helper.rb +7 -0
  25. data/app/helpers/collavre/public_assets_helper.rb +14 -0
  26. data/app/javascript/components/InlineLexicalEditor.jsx +42 -59
  27. data/app/javascript/components/plugins/attachment_cleanup_plugin.jsx +3 -0
  28. data/app/javascript/components/plugins/image_upload_plugin.jsx +12 -0
  29. data/app/javascript/controllers/__tests__/link_creative_controller.test.js +447 -0
  30. data/app/javascript/controllers/comment_controller.js +15 -1
  31. data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +108 -0
  32. data/app/javascript/controllers/comments/form_controller.js +4 -0
  33. data/app/javascript/controllers/comments/list_controller.js +27 -9
  34. data/app/javascript/controllers/comments/popup_controller.js +9 -0
  35. data/app/javascript/controllers/comments/presence_controller.js +137 -4
  36. data/app/javascript/controllers/comments/topics_controller.js +15 -0
  37. data/app/javascript/controllers/creatives/__tests__/sync_controller.test.js +89 -0
  38. data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +120 -0
  39. data/app/javascript/controllers/creatives/sync_controller.js +30 -9
  40. data/app/javascript/controllers/creatives/tree_controller.js +23 -0
  41. data/app/javascript/controllers/index.js +4 -1
  42. data/app/javascript/controllers/landing_video_controller.js +53 -0
  43. data/app/javascript/controllers/link_creative_controller.js +451 -29
  44. data/app/javascript/creatives/tree_renderer.js +6 -0
  45. data/app/javascript/lib/api/__tests__/queue_manager.test.js +27 -0
  46. data/app/javascript/lib/api/creatives.js +13 -0
  47. data/app/javascript/lib/api/queue_manager.js +17 -5
  48. data/app/javascript/lib/lexical/__tests__/color_import.test.js +318 -0
  49. data/app/javascript/lib/lexical/__tests__/minimize_html.test.js +259 -0
  50. data/app/javascript/lib/lexical/color_import.js +186 -0
  51. data/app/javascript/lib/lexical/minimize_html.js +182 -0
  52. data/app/javascript/lib/lexical/video_node.jsx +96 -0
  53. data/app/javascript/modules/__tests__/command_args_form.test.js +103 -0
  54. data/app/javascript/modules/__tests__/html_content_empty.test.js +41 -0
  55. data/app/javascript/modules/__tests__/markdown_source_reconcile.test.js +70 -0
  56. data/app/javascript/modules/command_args_form.js +22 -4
  57. data/app/javascript/modules/command_menu.js +27 -0
  58. data/app/javascript/modules/creative_row_editor.js +227 -17
  59. data/app/javascript/modules/html_content_empty.js +12 -0
  60. data/app/javascript/modules/markdown_source_reconcile.js +53 -0
  61. data/app/jobs/collavre/ai_agent_job.rb +89 -3
  62. data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +134 -0
  63. data/app/jobs/collavre/claude_channel_presence_job.rb +91 -0
  64. data/app/jobs/collavre/drop_trigger_job.rb +37 -8
  65. data/app/mailers/collavre/application_mailer.rb +1 -1
  66. data/app/models/collavre/agent_subscription.rb +52 -0
  67. data/app/models/collavre/channel/injected_message.rb +5 -0
  68. data/app/models/collavre/channel.rb +87 -0
  69. data/app/models/collavre/comment/claude_channel_permission.rb +145 -0
  70. data/app/models/collavre/comment.rb +70 -5
  71. data/app/models/collavre/creative/describable.rb +202 -3
  72. data/app/models/collavre/creative.rb +2 -0
  73. data/app/models/collavre/creative_share.rb +1 -0
  74. data/app/models/collavre/integration_setting.rb +35 -0
  75. data/app/models/collavre/preview_channel.rb +93 -0
  76. data/app/models/collavre/system_setting.rb +13 -2
  77. data/app/models/collavre/task.rb +34 -5
  78. data/app/models/collavre/topic.rb +8 -25
  79. data/app/models/collavre/user.rb +4 -0
  80. data/app/models/concerns/collavre/ai_agent_resolvable.rb +12 -3
  81. data/app/services/collavre/agent_session_abort.rb +28 -0
  82. data/app/services/collavre/ai_agent/claude_channel_adapter.rb +79 -0
  83. data/app/services/collavre/ai_agent/response_finalizer.rb +4 -2
  84. data/app/services/collavre/ai_agent_service.rb +68 -49
  85. data/app/services/collavre/ai_client.rb +3 -3
  86. data/app/services/collavre/attachment_backfill.rb +26 -0
  87. data/app/services/collavre/channel_attacher.rb +58 -0
  88. data/app/services/collavre/comments/mcp_command.rb +31 -1
  89. data/app/services/collavre/creatives/breadcrumb_resolver.rb +91 -0
  90. data/app/services/collavre/creatives/filter_pipeline.rb +26 -42
  91. data/app/services/collavre/creatives/filters/search_filter.rb +12 -2
  92. data/app/services/collavre/creatives/index_query.rb +110 -8
  93. data/app/services/collavre/creatives/permission_filter.rb +50 -0
  94. data/app/services/collavre/creatives/reveal_path_resolver.rb +118 -0
  95. data/app/services/collavre/creatives/tree_builder.rb +7 -3
  96. data/app/services/collavre/crons/recurring_task_arguments.rb +28 -0
  97. data/app/services/collavre/google_calendar_service.rb +4 -2
  98. data/app/services/collavre/markdown_converter.rb +130 -15
  99. data/app/services/collavre/markdown_importer.rb +7 -2
  100. data/app/services/collavre/orchestration/policy_resolver.rb +11 -1
  101. data/app/services/collavre/orchestration/stuck_detector.rb +22 -2
  102. data/app/services/collavre/tools/creative_attach_files_service.rb +62 -0
  103. data/app/services/collavre/tools/creative_list_attachments_service.rb +42 -0
  104. data/app/services/collavre/tools/creative_remove_attachment_service.rb +37 -0
  105. data/app/services/collavre/tools/cron_list_service.rb +1 -14
  106. data/app/services/collavre/tools/permission_denied_error.rb +9 -0
  107. data/app/services/collavre/tools/preview_attach_service.rb +128 -0
  108. data/app/services/collavre/tools/preview_detach_service.rb +61 -0
  109. data/app/services/collavre/tools/topic_authorizer.rb +24 -0
  110. data/app/services/collavre/topic_branch_service.rb +34 -26
  111. data/app/services/collavre/topics/orphaned_cron_notifier.rb +68 -0
  112. data/app/views/admin/shared/_tabs.html.erb +1 -0
  113. data/app/views/collavre/admin/integrations/_category.html.erb +22 -0
  114. data/app/views/collavre/admin/integrations/_setting_row.html.erb +70 -0
  115. data/app/views/collavre/admin/integrations/index.html.erb +42 -0
  116. data/app/views/collavre/admin/settings/_system_tab.html.erb +8 -0
  117. data/app/views/collavre/comments/_channel_chips.html.erb +33 -0
  118. data/app/views/collavre/comments/_comment.html.erb +16 -2
  119. data/app/views/collavre/comments/_comments_popup.html.erb +4 -1
  120. data/app/views/collavre/creatives/_inline_edit_form.html.erb +19 -0
  121. data/app/views/collavre/creatives/index.html.erb +10 -2
  122. data/app/views/collavre/landing/show.html.erb +130 -0
  123. data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -2
  124. data/app/views/layouts/collavre/landing.html.erb +33 -0
  125. data/config/locales/admin.en.yml +4 -2
  126. data/config/locales/admin.ko.yml +4 -2
  127. data/config/locales/channels.en.yml +13 -0
  128. data/config/locales/channels.ko.yml +13 -0
  129. data/config/locales/claude_channel.en.yml +16 -0
  130. data/config/locales/claude_channel.ko.yml +16 -0
  131. data/config/locales/comments.en.yml +5 -0
  132. data/config/locales/comments.ko.yml +5 -0
  133. data/config/locales/creatives.en.yml +11 -0
  134. data/config/locales/creatives.ko.yml +10 -0
  135. data/config/locales/integrations.en.yml +55 -0
  136. data/config/locales/integrations.ko.yml +55 -0
  137. data/config/locales/landing.en.yml +51 -0
  138. data/config/locales/landing.ko.yml +51 -0
  139. data/config/routes.rb +30 -0
  140. data/db/migrate/20260526000000_create_channels.rb +42 -0
  141. data/db/migrate/20260527000000_add_dismissed_at_to_channels.rb +6 -0
  142. data/db/migrate/20260527000100_backfill_dismissed_at_for_legacy_detached_channels.rb +28 -0
  143. data/db/migrate/20260528000000_add_preview_channel_unique_index.rb +31 -0
  144. data/db/migrate/20260529000000_add_primary_agent_id_to_topics.rb +40 -0
  145. data/db/migrate/20260529100000_create_integration_settings.rb +15 -0
  146. data/db/migrate/20260609000000_drop_action_text_rich_texts.rb +20 -0
  147. data/db/migrate/20260609005000_add_session_id_to_topics.rb +16 -0
  148. data/db/migrate/20260609010000_create_agent_subscriptions.rb +19 -0
  149. data/db/migrate/20260609020000_add_last_seen_at_to_agent_subscriptions.rb +23 -0
  150. data/db/migrate/20260609030000_add_session_id_to_agent_subscriptions.rb +17 -0
  151. data/db/migrate/20260609190659_backfill_creative_files_into_description.rb +24 -0
  152. data/db/seeds.rb +19 -0
  153. data/lib/collavre/aws_credentials.rb +75 -0
  154. data/lib/collavre/engine.rb +50 -0
  155. data/lib/collavre/integration_settings/key_definition.rb +35 -0
  156. data/lib/collavre/integration_settings/registry.rb +60 -0
  157. data/lib/collavre/integration_settings/resolver.rb +71 -0
  158. data/lib/collavre/integration_settings.rb +46 -0
  159. data/lib/collavre/ses_settings_interceptor.rb +72 -0
  160. data/lib/collavre/version.rb +1 -1
  161. data/lib/collavre.rb +3 -0
  162. metadata +82 -2
  163. data/app/services/collavre/openclaw_abort_service.rb +0 -45
@@ -4,22 +4,15 @@ module Collavre
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  def approve
7
+ # Claude Channel permission prompts reuse the approval comment UI but the
8
+ # tool runs inside the remote Claude Code process — never execute it
9
+ # server-side. Approving relays an "allow" decision to the suspended
10
+ # session instead of invoking the ActionExecutor.
11
+ return decide_claude_channel_permission(:allow) if @comment.claude_channel_permission?
12
+
7
13
  status = @comment.approval_status(Current.user)
8
14
  if status != :ok
9
- error_key = case status
10
- when :invalid_action_format then "collavre.comments.approve_invalid_format"
11
- when :missing_action then "collavre.comments.approve_missing_action"
12
- when :missing_approver then "collavre.comments.approve_missing_approver"
13
- when :admin_required then "collavre.comments.approve_admin_required"
14
- else "collavre.comments.approve_not_allowed"
15
- end
16
- http_status = case status
17
- when :invalid_action_format, :missing_action, :missing_approver
18
- :unprocessable_entity
19
- else
20
- :forbidden
21
- end
22
- render json: { error: I18n.t(error_key) }, status: http_status and return
15
+ render_approval_status_error(status) and return
23
16
  end
24
17
 
25
18
  begin
@@ -31,6 +24,17 @@ module Collavre
31
24
  end
32
25
  end
33
26
 
27
+ # Reject a Claude Channel tool-permission prompt. There is no native
28
+ # equivalent (the native approval UI has approve-only; an un-approved
29
+ # action is simply left pending), so deny is exclusive to these comments.
30
+ def deny
31
+ unless @comment.claude_channel_permission?
32
+ render json: { error: I18n.t("collavre.comments.approve_not_allowed") }, status: :forbidden and return
33
+ end
34
+
35
+ decide_claude_channel_permission(:deny)
36
+ end
37
+
34
38
  def update_action
35
39
  # Initial checks outside the lock
36
40
  executed_error = false
@@ -108,6 +112,45 @@ module Collavre
108
112
  rescue ::Comments::ActionValidator::ValidationError => e
109
113
  render json: { error: e.message }, status: :unprocessable_entity
110
114
  end
115
+
116
+ private
117
+
118
+ # Resolve a Claude Channel permission prompt: gate on the approver, record
119
+ # the decision atomically (idempotent against double-clicks), relay it to
120
+ # the suspended session, and re-render the now-decided comment.
121
+ def decide_claude_channel_permission(behavior)
122
+ status = @comment.approval_status(Current.user)
123
+ if status != :ok
124
+ render_approval_status_error(status) and return
125
+ end
126
+
127
+ begin
128
+ @comment.decide_claude_channel_permission!(behavior, by: Current.user)
129
+ rescue Comment::ClaudeChannelPermission::AlreadyDecided
130
+ render json: { error: I18n.t("collavre.comments.approve_already_executed") }, status: :unprocessable_entity and return
131
+ end
132
+
133
+ @comment.broadcast_claude_channel_permission_decision(behavior)
134
+ @comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
135
+ render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }
136
+ end
137
+
138
+ def render_approval_status_error(status)
139
+ error_key = case status
140
+ when :invalid_action_format then "collavre.comments.approve_invalid_format"
141
+ when :missing_action then "collavre.comments.approve_missing_action"
142
+ when :missing_approver then "collavre.comments.approve_missing_approver"
143
+ when :admin_required then "collavre.comments.approve_admin_required"
144
+ else "collavre.comments.approve_not_allowed"
145
+ end
146
+ http_status = case status
147
+ when :invalid_action_format, :missing_action, :missing_approver
148
+ :unprocessable_entity
149
+ else
150
+ :forbidden
151
+ end
152
+ render json: { error: I18n.t(error_key) }, status: http_status
153
+ end
111
154
  end
112
155
  end
113
156
  end
@@ -5,5 +5,12 @@ module Collavre
5
5
  rescue JSON::ParserError, TypeError
6
6
  comment.action.to_s
7
7
  end
8
+
9
+ def comment_action_markdown(comment)
10
+ parsed = JSON.parse(comment.action)
11
+ parsed["markdown"] if parsed.is_a?(Hash) && parsed["markdown"].present?
12
+ rescue JSON::ParserError, TypeError
13
+ nil
14
+ end
8
15
  end
9
16
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module PublicAssetsHelper
5
+ # Returns a URL for serving an ActiveStorage blob through the public-assets
6
+ # proxy. When PUBLIC_ASSETS_HOST is set (e.g. CloudFront domain) the URL is
7
+ # absolute; otherwise relative so the browser uses the current host.
8
+ def public_asset_url(blob)
9
+ path = "/public-assets/blobs/#{blob.signed_id}/#{blob.filename.sanitized}"
10
+ host = Collavre::IntegrationSettings.fetch(:public_assets_host)
11
+ host ? "#{host.chomp('/')}#{path}" : path
12
+ end
13
+ end
14
+ end
@@ -27,6 +27,7 @@ import {
27
27
  $getRoot,
28
28
  $getSelection,
29
29
  $isElementNode,
30
+ $isLineBreakNode,
30
31
  $isRangeSelection,
31
32
  $isTextNode,
32
33
  CAN_REDO_COMMAND,
@@ -47,9 +48,12 @@ import FileUploadPlugin, {
47
48
  } from "./plugins/image_upload_plugin"
48
49
  import { ImageNode } from "../lib/lexical/image_node"
49
50
  import { AttachmentNode } from "../lib/lexical/attachment_node"
51
+ import { VideoNode } from "../lib/lexical/video_node"
50
52
  import AttachmentCleanupPlugin from "./plugins/attachment_cleanup_plugin"
51
53
  import MarkdownShortcutsPlugin from "./plugins/markdown_shortcuts_plugin"
52
54
  import { syncLexicalStyleAttributes } from "../lib/lexical/style_attributes"
55
+ import { lexicalHtmlConfig, normalizeColoredContainers } from "../lib/lexical/color_import"
56
+ import { minimizeContentHtml } from "../lib/lexical/minimize_html"
53
57
  import { updateResponsiveImages } from "../lib/responsive_images"
54
58
 
55
59
  const URL_MATCHERS = [
@@ -120,43 +124,6 @@ function InitialContentPlugin({ html }) {
120
124
  const [editor] = useLexicalComposerContext()
121
125
  const lastApplied = useRef(null)
122
126
 
123
- const collectDomTextStyles = useCallback((container) => {
124
- const styles = []
125
- if (!container) return styles
126
- const ownerDocument = container.ownerDocument || document
127
- const walker = ownerDocument.createTreeWalker(container, NodeFilter.SHOW_TEXT)
128
- let current = walker.nextNode()
129
- while (current) {
130
- const parent = current.parentElement
131
- let styleText = parent?.getAttribute?.("style") || ""
132
- const colorAttr = parent?.dataset?.lexicalColor
133
- const bgAttr = parent?.dataset?.lexicalBackgroundColor
134
-
135
- if ((!styleText || !styleText.trim()) && (colorAttr || bgAttr)) {
136
- const declarations = []
137
- if (colorAttr) declarations.push(`color: ${colorAttr}`)
138
- if (bgAttr) declarations.push(`background-color: ${bgAttr}`)
139
- styleText = declarations.join("; ")
140
- } else {
141
- const lower = styleText.toLowerCase()
142
- const fragments = []
143
- if (colorAttr && !lower.includes("color:")) {
144
- fragments.push(`color: ${colorAttr}`)
145
- }
146
- if (bgAttr && !lower.includes("background-color:")) {
147
- fragments.push(`background-color: ${bgAttr}`)
148
- }
149
- if (fragments.length > 0) {
150
- styleText = `${styleText}${styleText.trim().endsWith(";") || !styleText.trim() ? "" : ";"} ${fragments.join("; ")}`.trim()
151
- }
152
- }
153
-
154
- styles.push(styleText || "")
155
- current = walker.nextNode()
156
- }
157
- return styles
158
- }, [])
159
-
160
127
  useEffect(() => {
161
128
  if (lastApplied.current === html) return
162
129
  lastApplied.current = html
@@ -170,8 +137,15 @@ function InitialContentPlugin({ html }) {
170
137
  // No more .trix-content wrapper
171
138
  const container = doc.body
172
139
 
140
+ // Color / background-color are bound to text nodes during import by the
141
+ // colorAwareSpanImport html config (see lib/lexical/color_import). We no
142
+ // longer re-apply styles positionally after import, which used to drift
143
+ // onto the wrong text node whenever Lexical split or dropped text nodes.
173
144
  syncLexicalStyleAttributes(container)
174
- const collectedStyles = collectDomTextStyles(container)
145
+ // Push color/background-color from non-span elements onto spans so the
146
+ // colorAwareSpanImport config binds it (the span importer can't see it
147
+ // otherwise). Must run after the sync above materializes data-lexical-*.
148
+ normalizeColoredContainers(container)
175
149
  const nodes = $generateNodesFromDOM(editor, container)
176
150
 
177
151
  // Filter out duplicate image nodes if any
@@ -192,24 +166,35 @@ function InitialContentPlugin({ html }) {
192
166
  })
193
167
 
194
168
  const appendedNodes = []
195
- uniqueNodes.forEach((node) => {
196
- if ($isTextNode(node)) {
197
- const paragraph = $createParagraphNode()
198
- paragraph.append(node)
199
- root.append(paragraph)
200
- appendedNodes.push(paragraph)
201
- return
169
+ // Text nodes and inline elements (links, etc.) cannot live directly under
170
+ // the root. Minimized HTML stores a single line without its <p> wrapper, so
171
+ // a line like "Hello <strong>World</strong>" re-imports as several
172
+ // top-level inline nodes — group consecutive ones back into one paragraph
173
+ // so the line is not split apart.
174
+ let pendingParagraph = null
175
+ const flushPending = () => {
176
+ if (pendingParagraph) {
177
+ root.append(pendingParagraph)
178
+ appendedNodes.push(pendingParagraph)
179
+ pendingParagraph = null
202
180
  }
203
-
204
- if ($isElementNode(node) && node.getType?.() === "paragraph") {
205
- root.append(node)
206
- appendedNodes.push(node)
181
+ }
182
+ uniqueNodes.forEach((node) => {
183
+ const isInlineLeaf =
184
+ $isTextNode(node) ||
185
+ $isLineBreakNode(node) ||
186
+ ($isElementNode(node) && node.isInline())
187
+ if (isInlineLeaf) {
188
+ if (!pendingParagraph) pendingParagraph = $createParagraphNode()
189
+ pendingParagraph.append(node)
207
190
  return
208
191
  }
209
192
 
193
+ flushPending()
210
194
  root.append(node)
211
195
  appendedNodes.push(node)
212
196
  })
197
+ flushPending()
213
198
 
214
199
  if (root.getChildrenSize() === 0) {
215
200
  const paragraph = $createParagraphNode()
@@ -217,12 +202,6 @@ function InitialContentPlugin({ html }) {
217
202
  appendedNodes.push(paragraph)
218
203
  }
219
204
 
220
- const textNodes = root.getAllTextNodes()
221
- textNodes.forEach((textNode, index) => {
222
- const style = collectedStyles[index]
223
- textNode.setStyle(style || "")
224
- })
225
-
226
205
  let lastChild = root.getLastChild()
227
206
  while (
228
207
  lastChild &&
@@ -237,7 +216,7 @@ function InitialContentPlugin({ html }) {
237
216
  root.append($createParagraphNode())
238
217
  }
239
218
  })
240
- }, [collectDomTextStyles, editor, html])
219
+ }, [editor, html])
241
220
 
242
221
  return null
243
222
  }
@@ -942,7 +921,9 @@ function EditorInner({
942
921
  anchor.setAttribute("target", "_blank")
943
922
  anchor.setAttribute("rel", "noopener")
944
923
  })
945
- serialized = doc.body.innerHTML
924
+ // Strip Lexical's verbose markup (extra <div>, white-space spans,
925
+ // duplicate format wrappers, single-line <p>) before persisting.
926
+ serialized = minimizeContentHtml(doc.body.firstElementChild)
946
927
  })
947
928
  // No Trix wrapper
948
929
  onChange(serialized)
@@ -1019,12 +1000,14 @@ export default function InlineLexicalEditor({
1019
1000
  LinkNode,
1020
1001
  AutoLinkNode,
1021
1002
  ImageNode,
1022
- AttachmentNode
1003
+ AttachmentNode,
1004
+ VideoNode
1023
1005
  ],
1024
1006
  onError(error) {
1025
1007
  throw error
1026
1008
  },
1027
- theme
1009
+ theme,
1010
+ html: lexicalHtmlConfig
1028
1011
  }),
1029
1012
  []
1030
1013
  )
@@ -3,6 +3,7 @@ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext
3
3
  import { $getRoot } from "lexical"
4
4
  import { $isImageNode } from "../../lib/lexical/image_node"
5
5
  import { $isAttachmentNode } from "../../lib/lexical/attachment_node"
6
+ import { $isVideoNode } from "../../lib/lexical/video_node"
6
7
 
7
8
  function extractSignedIdFromUrl(url) {
8
9
  if (!url) return null
@@ -27,6 +28,8 @@ function getAllAttachmentUrls(editor) {
27
28
  function traverse(node) {
28
29
  if ($isImageNode(node)) {
29
30
  urls.add(node.getSrc())
31
+ } else if ($isVideoNode(node)) {
32
+ urls.add(node.getSrc())
30
33
  } else if ($isAttachmentNode(node)) {
31
34
  urls.add(node.getSrc())
32
35
  }
@@ -15,6 +15,7 @@ import {
15
15
 
16
16
  import { $createImageNode } from "../../lib/lexical/image_node"
17
17
  import { $createAttachmentNode } from "../../lib/lexical/attachment_node"
18
+ import { $createVideoNode } from "../../lib/lexical/video_node"
18
19
 
19
20
  export const INSERT_IMAGE_COMMAND = createCommand("INSERT_IMAGE_COMMAND")
20
21
  export const INSERT_FILE_COMMAND = createCommand("INSERT_FILE_COMMAND")
@@ -25,6 +26,12 @@ function isImageFile(file) {
25
26
  return /\.(bmp|gif|jpe?g|png|svg|webp)$/i.test(file.name || "")
26
27
  }
27
28
 
29
+ function isVideoFile(file) {
30
+ if (!file) return false
31
+ if (file.type) return /^video\//i.test(file.type)
32
+ return /\.(mp4|webm|mov|m4v)$/i.test(file.name || "")
33
+ }
34
+
28
35
  export default function FileUploadPlugin({
29
36
  onUploadStateChange,
30
37
  directUploadUrl,
@@ -78,6 +85,11 @@ export default function FileUploadPlugin({
78
85
  altText: attributes.filename,
79
86
  maxWidth: 800 // Default max width
80
87
  })
88
+ } else if (isVideoFile(file)) {
89
+ node = $createVideoNode({
90
+ src: url,
91
+ filename: attributes.filename
92
+ })
81
93
  } else {
82
94
  node = $createAttachmentNode({
83
95
  src: url,