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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/collavre/actiontext.css +92 -2
- data/app/assets/stylesheets/collavre/code_highlight.css +144 -26
- data/app/assets/stylesheets/collavre/comments_popup.css +133 -2
- data/app/assets/stylesheets/collavre/landing.css +507 -0
- data/app/assets/stylesheets/collavre/popup.css +148 -0
- data/app/channels/collavre/agent_channel.rb +205 -0
- data/app/channels/collavre/comments_presence_channel.rb +7 -0
- data/app/controllers/collavre/admin/integrations_controller.rb +93 -0
- data/app/controllers/collavre/admin/settings_controller.rb +22 -17
- data/app/controllers/collavre/api/v1/agents_controller.rb +777 -0
- data/app/controllers/collavre/api/v1/base_controller.rb +46 -0
- data/app/controllers/collavre/application_controller.rb +27 -0
- data/app/controllers/collavre/attachments_controller.rb +30 -2
- data/app/controllers/collavre/channels_controller.rb +23 -0
- data/app/controllers/collavre/comments_controller.rb +1 -1
- data/app/controllers/collavre/creatives/attachments_controller.rb +79 -0
- data/app/controllers/collavre/creatives_controller.rb +141 -7
- data/app/controllers/collavre/landing_controller.rb +8 -0
- data/app/controllers/collavre/public_assets_controller.rb +24 -0
- data/app/controllers/collavre/tasks_controller.rb +12 -4
- data/app/controllers/collavre/topics_controller.rb +36 -30
- data/app/controllers/concerns/collavre/comments/approval_actions.rb +57 -14
- data/app/helpers/collavre/comments_helper.rb +7 -0
- data/app/helpers/collavre/public_assets_helper.rb +14 -0
- data/app/javascript/components/InlineLexicalEditor.jsx +42 -59
- data/app/javascript/components/plugins/attachment_cleanup_plugin.jsx +3 -0
- data/app/javascript/components/plugins/image_upload_plugin.jsx +12 -0
- data/app/javascript/controllers/__tests__/link_creative_controller.test.js +447 -0
- data/app/javascript/controllers/comment_controller.js +15 -1
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +108 -0
- data/app/javascript/controllers/comments/form_controller.js +4 -0
- data/app/javascript/controllers/comments/list_controller.js +27 -9
- data/app/javascript/controllers/comments/popup_controller.js +9 -0
- data/app/javascript/controllers/comments/presence_controller.js +137 -4
- data/app/javascript/controllers/comments/topics_controller.js +15 -0
- data/app/javascript/controllers/creatives/__tests__/sync_controller.test.js +89 -0
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +120 -0
- data/app/javascript/controllers/creatives/sync_controller.js +30 -9
- data/app/javascript/controllers/creatives/tree_controller.js +23 -0
- data/app/javascript/controllers/index.js +4 -1
- data/app/javascript/controllers/landing_video_controller.js +53 -0
- data/app/javascript/controllers/link_creative_controller.js +451 -29
- data/app/javascript/creatives/tree_renderer.js +6 -0
- data/app/javascript/lib/api/__tests__/queue_manager.test.js +27 -0
- data/app/javascript/lib/api/creatives.js +13 -0
- data/app/javascript/lib/api/queue_manager.js +17 -5
- data/app/javascript/lib/lexical/__tests__/color_import.test.js +318 -0
- data/app/javascript/lib/lexical/__tests__/minimize_html.test.js +259 -0
- data/app/javascript/lib/lexical/color_import.js +186 -0
- data/app/javascript/lib/lexical/minimize_html.js +182 -0
- data/app/javascript/lib/lexical/video_node.jsx +96 -0
- data/app/javascript/modules/__tests__/command_args_form.test.js +103 -0
- data/app/javascript/modules/__tests__/html_content_empty.test.js +41 -0
- data/app/javascript/modules/__tests__/markdown_source_reconcile.test.js +70 -0
- data/app/javascript/modules/command_args_form.js +22 -4
- data/app/javascript/modules/command_menu.js +27 -0
- data/app/javascript/modules/creative_row_editor.js +227 -17
- data/app/javascript/modules/html_content_empty.js +12 -0
- data/app/javascript/modules/markdown_source_reconcile.js +53 -0
- data/app/jobs/collavre/ai_agent_job.rb +89 -3
- data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +134 -0
- data/app/jobs/collavre/claude_channel_presence_job.rb +91 -0
- data/app/jobs/collavre/drop_trigger_job.rb +37 -8
- data/app/mailers/collavre/application_mailer.rb +1 -1
- data/app/models/collavre/agent_subscription.rb +52 -0
- data/app/models/collavre/channel/injected_message.rb +5 -0
- data/app/models/collavre/channel.rb +87 -0
- data/app/models/collavre/comment/claude_channel_permission.rb +145 -0
- data/app/models/collavre/comment.rb +70 -5
- data/app/models/collavre/creative/describable.rb +202 -3
- data/app/models/collavre/creative.rb +2 -0
- data/app/models/collavre/creative_share.rb +1 -0
- data/app/models/collavre/integration_setting.rb +35 -0
- data/app/models/collavre/preview_channel.rb +93 -0
- data/app/models/collavre/system_setting.rb +13 -2
- data/app/models/collavre/task.rb +34 -5
- data/app/models/collavre/topic.rb +8 -25
- data/app/models/collavre/user.rb +4 -0
- data/app/models/concerns/collavre/ai_agent_resolvable.rb +12 -3
- data/app/services/collavre/agent_session_abort.rb +28 -0
- data/app/services/collavre/ai_agent/claude_channel_adapter.rb +79 -0
- data/app/services/collavre/ai_agent/response_finalizer.rb +4 -2
- data/app/services/collavre/ai_agent_service.rb +68 -49
- data/app/services/collavre/ai_client.rb +3 -3
- data/app/services/collavre/attachment_backfill.rb +26 -0
- data/app/services/collavre/channel_attacher.rb +58 -0
- data/app/services/collavre/comments/mcp_command.rb +31 -1
- data/app/services/collavre/creatives/breadcrumb_resolver.rb +91 -0
- data/app/services/collavre/creatives/filter_pipeline.rb +26 -42
- data/app/services/collavre/creatives/filters/search_filter.rb +12 -2
- data/app/services/collavre/creatives/index_query.rb +110 -8
- data/app/services/collavre/creatives/permission_filter.rb +50 -0
- data/app/services/collavre/creatives/reveal_path_resolver.rb +118 -0
- data/app/services/collavre/creatives/tree_builder.rb +7 -3
- data/app/services/collavre/crons/recurring_task_arguments.rb +28 -0
- data/app/services/collavre/google_calendar_service.rb +4 -2
- data/app/services/collavre/markdown_converter.rb +130 -15
- data/app/services/collavre/markdown_importer.rb +7 -2
- data/app/services/collavre/orchestration/policy_resolver.rb +11 -1
- data/app/services/collavre/orchestration/stuck_detector.rb +22 -2
- data/app/services/collavre/tools/creative_attach_files_service.rb +62 -0
- data/app/services/collavre/tools/creative_list_attachments_service.rb +42 -0
- data/app/services/collavre/tools/creative_remove_attachment_service.rb +37 -0
- data/app/services/collavre/tools/cron_list_service.rb +1 -14
- data/app/services/collavre/tools/permission_denied_error.rb +9 -0
- data/app/services/collavre/tools/preview_attach_service.rb +128 -0
- data/app/services/collavre/tools/preview_detach_service.rb +61 -0
- data/app/services/collavre/tools/topic_authorizer.rb +24 -0
- data/app/services/collavre/topic_branch_service.rb +34 -26
- data/app/services/collavre/topics/orphaned_cron_notifier.rb +68 -0
- data/app/views/admin/shared/_tabs.html.erb +1 -0
- data/app/views/collavre/admin/integrations/_category.html.erb +22 -0
- data/app/views/collavre/admin/integrations/_setting_row.html.erb +70 -0
- data/app/views/collavre/admin/integrations/index.html.erb +42 -0
- data/app/views/collavre/admin/settings/_system_tab.html.erb +8 -0
- data/app/views/collavre/comments/_channel_chips.html.erb +33 -0
- data/app/views/collavre/comments/_comment.html.erb +16 -2
- data/app/views/collavre/comments/_comments_popup.html.erb +4 -1
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +19 -0
- data/app/views/collavre/creatives/index.html.erb +10 -2
- data/app/views/collavre/landing/show.html.erb +130 -0
- data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -2
- data/app/views/layouts/collavre/landing.html.erb +33 -0
- data/config/locales/admin.en.yml +4 -2
- data/config/locales/admin.ko.yml +4 -2
- data/config/locales/channels.en.yml +13 -0
- data/config/locales/channels.ko.yml +13 -0
- data/config/locales/claude_channel.en.yml +16 -0
- data/config/locales/claude_channel.ko.yml +16 -0
- data/config/locales/comments.en.yml +5 -0
- data/config/locales/comments.ko.yml +5 -0
- data/config/locales/creatives.en.yml +11 -0
- data/config/locales/creatives.ko.yml +10 -0
- data/config/locales/integrations.en.yml +55 -0
- data/config/locales/integrations.ko.yml +55 -0
- data/config/locales/landing.en.yml +51 -0
- data/config/locales/landing.ko.yml +51 -0
- data/config/routes.rb +30 -0
- data/db/migrate/20260526000000_create_channels.rb +42 -0
- data/db/migrate/20260527000000_add_dismissed_at_to_channels.rb +6 -0
- data/db/migrate/20260527000100_backfill_dismissed_at_for_legacy_detached_channels.rb +28 -0
- data/db/migrate/20260528000000_add_preview_channel_unique_index.rb +31 -0
- data/db/migrate/20260529000000_add_primary_agent_id_to_topics.rb +40 -0
- data/db/migrate/20260529100000_create_integration_settings.rb +15 -0
- data/db/migrate/20260609000000_drop_action_text_rich_texts.rb +20 -0
- data/db/migrate/20260609005000_add_session_id_to_topics.rb +16 -0
- data/db/migrate/20260609010000_create_agent_subscriptions.rb +19 -0
- data/db/migrate/20260609020000_add_last_seen_at_to_agent_subscriptions.rb +23 -0
- data/db/migrate/20260609030000_add_session_id_to_agent_subscriptions.rb +17 -0
- data/db/migrate/20260609190659_backfill_creative_files_into_description.rb +24 -0
- data/db/seeds.rb +19 -0
- data/lib/collavre/aws_credentials.rb +75 -0
- data/lib/collavre/engine.rb +50 -0
- data/lib/collavre/integration_settings/key_definition.rb +35 -0
- data/lib/collavre/integration_settings/registry.rb +60 -0
- data/lib/collavre/integration_settings/resolver.rb +71 -0
- data/lib/collavre/integration_settings.rb +46 -0
- data/lib/collavre/ses_settings_interceptor.rb +72 -0
- data/lib/collavre/version.rb +1 -1
- data/lib/collavre.rb +3 -0
- metadata +82 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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,
|