collavre 0.21.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.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/app/assets/stylesheets/collavre/actiontext.css +251 -90
- data/app/assets/stylesheets/collavre/code_highlight.css +7 -201
- data/app/assets/stylesheets/collavre/comments_popup.css +169 -64
- data/app/assets/stylesheets/collavre/creatives.css +11 -2
- data/app/assets/stylesheets/collavre/modal_dialog.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +148 -0
- data/app/assets/stylesheets/collavre/tables.css +91 -0
- data/app/channels/collavre/agent_channel.rb +205 -0
- data/app/channels/collavre/inbox_badge_channel.rb +30 -0
- data/app/controllers/collavre/admin/integrations_controller.rb +16 -5
- 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/api/v1/mobile/agent_events_controller.rb +224 -0
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +95 -0
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +31 -0
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +25 -0
- data/app/controllers/collavre/attachments_controller.rb +30 -2
- 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 +107 -6
- data/app/controllers/collavre/tasks_controller.rb +21 -4
- data/app/controllers/collavre/topics_controller.rb +64 -1
- data/app/controllers/collavre/typo_corrections_controller.rb +39 -0
- data/app/controllers/concerns/collavre/comments/approval_actions.rb +57 -14
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +16 -1
- data/app/controllers/concerns/collavre/users_controller/registration.rb +41 -1
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/javascript/collavre.js +2 -0
- data/app/javascript/components/ImageResizer.jsx +9 -3
- data/app/javascript/components/InlineLexicalEditor.jsx +196 -96
- data/app/javascript/components/creative_tree_row.js +20 -3
- 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/components/plugins/list_tab_indent_plugin.jsx +16 -0
- data/app/javascript/components/plugins/table_hover_actions_plugin.jsx +405 -0
- data/app/javascript/controllers/__tests__/inbox_badge_controller.test.js +73 -0
- data/app/javascript/controllers/__tests__/link_creative_controller.test.js +447 -0
- data/app/javascript/controllers/comment_controller.js +11 -5
- data/app/javascript/controllers/comment_version_controller.js +2 -1
- data/app/javascript/controllers/comments/__tests__/form_controller_double_submit.test.js +159 -0
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +111 -2
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +94 -0
- data/app/javascript/controllers/comments/form_controller.js +21 -5
- data/app/javascript/controllers/comments/list_controller.js +35 -19
- data/app/javascript/controllers/comments/presence_controller.js +58 -6
- data/app/javascript/controllers/comments/topics_controller.js +14 -8
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +150 -0
- data/app/javascript/controllers/creatives/import_controller.js +2 -1
- data/app/javascript/controllers/creatives/select_mode_controller.js +2 -1
- data/app/javascript/controllers/creatives/tree_controller.js +142 -1
- data/app/javascript/controllers/image_lightbox_controller.js +2 -1
- data/app/javascript/controllers/inbox_badge_controller.js +33 -0
- data/app/javascript/controllers/index.js +4 -1
- data/app/javascript/controllers/link_creative_controller.js +451 -29
- data/app/javascript/controllers/share_modal_controller.js +4 -3
- data/app/javascript/controllers/topic_search_controller.js +2 -1
- data/app/javascript/creatives/drag_drop/event_handlers.js +14 -5
- data/app/javascript/creatives/topic_move_members_popup.js +156 -0
- data/app/javascript/creatives/tree_renderer.js +11 -0
- data/app/javascript/lib/__tests__/turbo_confirm.test.js +81 -0
- data/app/javascript/lib/__tests__/typo_correction.test.js +192 -0
- data/app/javascript/lib/api/__tests__/api_error.test.js +96 -0
- data/app/javascript/lib/api/__tests__/queue_manager.test.js +88 -1
- data/app/javascript/lib/api/api_error.js +108 -0
- data/app/javascript/lib/api/creatives.js +13 -0
- data/app/javascript/lib/api/queue_manager.js +38 -4
- data/app/javascript/lib/common_popup.js +18 -5
- data/app/javascript/lib/editor/__tests__/code_edit_view_token_parity.test.js +121 -0
- data/app/javascript/lib/editor/__tests__/code_language_roundtrip.test.js +152 -0
- data/app/javascript/lib/editor/__tests__/code_languages.test.js +93 -0
- data/app/javascript/lib/editor/code_languages.js +173 -0
- data/app/javascript/lib/editor/code_token_theme.js +41 -0
- data/app/javascript/lib/lexical/__tests__/color_import.test.js +318 -0
- data/app/javascript/lib/lexical/__tests__/image_focus.test.js +139 -0
- data/app/javascript/lib/lexical/__tests__/list_tab_indent.test.js +633 -0
- data/app/javascript/lib/lexical/__tests__/markdown_serialize.test.js +627 -0
- data/app/javascript/lib/lexical/__tests__/minimize_html.test.js +278 -0
- data/app/javascript/lib/lexical/__tests__/selection_boundary.test.js +88 -0
- data/app/javascript/lib/lexical/__tests__/table_transformer.test.js +163 -0
- data/app/javascript/lib/lexical/__tests__/trailing_paragraph.test.js +104 -0
- data/app/javascript/lib/lexical/color_import.js +186 -0
- data/app/javascript/lib/lexical/list_tab_indent.js +210 -0
- data/app/javascript/lib/lexical/markdown_serialize.js +320 -0
- data/app/javascript/lib/lexical/minimize_html.js +182 -0
- data/app/javascript/lib/lexical/selection_boundary.js +58 -0
- data/app/javascript/lib/lexical/table_transformer.js +182 -0
- data/app/javascript/lib/lexical/trailing_paragraph.js +29 -0
- data/app/javascript/lib/lexical/video_node.jsx +96 -0
- data/app/javascript/lib/turbo_confirm.js +46 -0
- data/app/javascript/lib/typo_correction.js +146 -0
- data/app/javascript/lib/utils/__tests__/confirm_dialog.test.js +88 -0
- data/app/javascript/lib/utils/__tests__/dialog.test.js +92 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +153 -0
- data/app/javascript/lib/utils/__tests__/sanitize_description.test.js +68 -0
- data/app/javascript/lib/utils/__tests__/table_download.test.js +93 -0
- data/app/javascript/lib/utils/confirm_dialog.js +10 -0
- data/app/javascript/lib/utils/dialog.js +300 -0
- data/app/javascript/lib/utils/markdown.js +154 -67
- data/app/javascript/lib/utils/sanitize_description.js +31 -0
- data/app/javascript/lib/utils/table_download.js +15 -0
- data/app/javascript/modules/__tests__/typo_corrector.test.js +365 -0
- data/app/javascript/modules/creative_row_editor.js +110 -70
- data/app/javascript/modules/export_to_markdown.js +2 -1
- data/app/javascript/modules/lexical_inline_editor.jsx +2 -1
- data/app/javascript/modules/slide_view.js +11 -2
- data/app/javascript/modules/typo_corrector.js +534 -0
- data/app/jobs/collavre/ai_agent_job.rb +92 -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/compress_job.rb +6 -2
- data/app/models/collavre/agent_subscription.rb +52 -0
- data/app/models/collavre/comment/broadcastable.rb +46 -7
- data/app/models/collavre/comment/claude_channel_permission.rb +145 -0
- data/app/models/collavre/comment/notifiable.rb +14 -4
- data/app/models/collavre/comment.rb +124 -11
- data/app/models/collavre/creative/describable.rb +220 -4
- data/app/models/collavre/creative_share.rb +1 -0
- data/app/models/collavre/task.rb +49 -5
- data/app/models/collavre/topic.rb +5 -0
- data/app/models/collavre/user.rb +61 -1
- 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 +28 -10
- data/app/services/collavre/attachment_backfill.rb +26 -0
- data/app/services/collavre/auto_theme_generator.rb +1 -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 +195 -24
- 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 +2 -1
- data/app/services/collavre/crons/recurring_task_arguments.rb +28 -0
- data/app/services/collavre/gemini_parent_recommender.rb +1 -1
- data/app/services/collavre/inbox_reply_service.rb +5 -0
- data/app/services/collavre/markdown_converter.rb +13 -3
- data/app/services/collavre/mobile/event_summarizer.rb +40 -0
- data/app/services/collavre/orchestration/agent_orchestrator.rb +33 -7
- data/app/services/collavre/orchestration/arbiter.rb +16 -0
- data/app/services/collavre/orchestration/matcher.rb +79 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +4 -3
- data/app/services/collavre/orchestration/stuck_detector.rb +146 -19
- data/app/services/collavre/tools/creative_attach_files_service.rb +29 -63
- data/app/services/collavre/tools/creative_batch_service.rb +3 -2
- data/app/services/collavre/tools/creative_create_service.rb +8 -8
- data/app/services/collavre/tools/creative_remove_attachment_service.rb +7 -5
- data/app/services/collavre/tools/creative_update_service.rb +23 -8
- data/app/services/collavre/tools/cron_list_service.rb +1 -14
- data/app/services/collavre/topics/orphaned_cron_notifier.rb +68 -0
- data/app/services/collavre/typo_corrector.rb +188 -0
- data/app/views/collavre/admin/integrations/_category.html.erb +1 -1
- data/app/views/collavre/admin/integrations/_setting_row.html.erb +27 -11
- data/app/views/collavre/comments/_comment.html.erb +15 -1
- data/app/views/collavre/comments/_comments_popup.html.erb +18 -3
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +1 -0
- data/app/views/collavre/creatives/_topic_move_members_modal.html.erb +42 -0
- data/app/views/collavre/creatives/index.html.erb +14 -1
- data/app/views/collavre/creatives/slide_view.html.erb +1 -1
- data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -2
- data/app/views/collavre/users/show.html.erb +3 -0
- data/app/views/collavre/users/typo_correction.html.erb +50 -0
- data/app/views/layouts/collavre/slide.html.erb +1 -0
- data/config/locales/channels.en.yml +2 -0
- data/config/locales/channels.ko.yml +2 -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 +18 -0
- data/config/locales/comments.ko.yml +18 -0
- data/config/locales/creatives.en.yml +2 -0
- data/config/locales/creatives.ko.yml +2 -0
- data/config/locales/integrations.en.yml +13 -2
- data/config/locales/integrations.ko.yml +13 -2
- data/config/locales/mobile.en.yml +16 -0
- data/config/locales/mobile.ko.yml +16 -0
- data/config/locales/orchestration.en.yml +1 -0
- data/config/locales/orchestration.ko.yml +1 -0
- data/config/locales/users.en.yml +15 -0
- data/config/locales/users.ko.yml +15 -0
- data/config/routes.rb +25 -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/migrate/20260612000000_add_topic_concurrency_defer_to_comments.rb +38 -0
- data/db/migrate/20260617090000_add_typo_correction_settings_to_users.rb +18 -0
- data/db/seeds.rb +51 -0
- data/lib/collavre/engine.rb +0 -1
- data/lib/collavre/integration_settings/key_definition.rb +6 -0
- data/lib/collavre/integration_settings/registry.rb +7 -2
- data/lib/collavre/version.rb +1 -1
- data/lib/generators/collavre/install/install_generator.rb +1 -0
- metadata +85 -3
- data/app/services/collavre/openclaw_abort_service.rb +0 -45
- data/app/services/collavre/tools/description_normalizable.rb +0 -16
|
@@ -21,12 +21,16 @@ import {
|
|
|
21
21
|
} from "@lexical/code"
|
|
22
22
|
import { ListItemNode, ListNode, $isListItemNode, $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from "@lexical/list"
|
|
23
23
|
import { $createLinkNode, LinkNode, AutoLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"
|
|
24
|
+
import { TableNode, TableRowNode, TableCellNode, INSERT_TABLE_COMMAND } from "@lexical/table"
|
|
25
|
+
import { TablePlugin } from "@lexical/react/LexicalTablePlugin"
|
|
26
|
+
import TableHoverActionsPlugin from "./plugins/table_hover_actions_plugin"
|
|
24
27
|
import {
|
|
25
28
|
$createParagraphNode,
|
|
26
29
|
$createTextNode,
|
|
27
30
|
$getRoot,
|
|
28
31
|
$getSelection,
|
|
29
32
|
$isElementNode,
|
|
33
|
+
$isLineBreakNode,
|
|
30
34
|
$isRangeSelection,
|
|
31
35
|
$isTextNode,
|
|
32
36
|
CAN_REDO_COMMAND,
|
|
@@ -47,10 +51,23 @@ import FileUploadPlugin, {
|
|
|
47
51
|
} from "./plugins/image_upload_plugin"
|
|
48
52
|
import { ImageNode } from "../lib/lexical/image_node"
|
|
49
53
|
import { AttachmentNode } from "../lib/lexical/attachment_node"
|
|
54
|
+
import { VideoNode } from "../lib/lexical/video_node"
|
|
50
55
|
import AttachmentCleanupPlugin from "./plugins/attachment_cleanup_plugin"
|
|
51
56
|
import MarkdownShortcutsPlugin from "./plugins/markdown_shortcuts_plugin"
|
|
57
|
+
import ListTabIndentPlugin from "./plugins/list_tab_indent_plugin"
|
|
52
58
|
import { syncLexicalStyleAttributes } from "../lib/lexical/style_attributes"
|
|
59
|
+
import { lexicalHtmlConfig, normalizeColoredContainers } from "../lib/lexical/color_import"
|
|
60
|
+
import { minimizeContentHtml } from "../lib/lexical/minimize_html"
|
|
61
|
+
import { ensureTrailingParagraph, registerTrailingParagraph } from "../lib/lexical/trailing_paragraph"
|
|
62
|
+
import {
|
|
63
|
+
MARKDOWN_TRANSFORMERS,
|
|
64
|
+
normalizeMarkdownBlankLines,
|
|
65
|
+
splitBlankLineParagraphs
|
|
66
|
+
} from "../lib/lexical/markdown_serialize"
|
|
67
|
+
import { $convertToMarkdownString } from "@lexical/markdown"
|
|
53
68
|
import { updateResponsiveImages } from "../lib/responsive_images"
|
|
69
|
+
import { CODE_TOKEN_THEME } from "../lib/editor/code_token_theme"
|
|
70
|
+
import { detectCodeLanguage, normalizeFenceLang, bridgeCodeFenceLanguages, markLanguageResolved, isLanguageResolved, clearLanguageResolved } from "../lib/editor/code_languages"
|
|
54
71
|
|
|
55
72
|
const URL_MATCHERS = [
|
|
56
73
|
createLinkMatcherWithRegExp(/https?:\/\/[^\s<]+/gi, (text) => text)
|
|
@@ -67,40 +84,14 @@ const theme = {
|
|
|
67
84
|
list: {
|
|
68
85
|
ul: "lexical-list-ul",
|
|
69
86
|
ol: "lexical-list-ol",
|
|
70
|
-
listitem: "lexical-list-item"
|
|
87
|
+
listitem: "lexical-list-item",
|
|
88
|
+
// Tag the wrapper <li> that only holds a nested list so its bullet marker
|
|
89
|
+
// can be hidden — without this Lexical reuses the plain item class and the
|
|
90
|
+
// empty wrapper renders a stray bullet above the indented sub-list.
|
|
91
|
+
nested: { listitem: "lexical-nested-list-item" }
|
|
71
92
|
},
|
|
72
93
|
code: "lexical-code-block",
|
|
73
|
-
codeHighlight:
|
|
74
|
-
atrule: "lexical-token-atrule",
|
|
75
|
-
attr: "lexical-token-attr",
|
|
76
|
-
boolean: "lexical-token-boolean",
|
|
77
|
-
builtin: "lexical-token-builtin",
|
|
78
|
-
cdata: "lexical-token-cdata",
|
|
79
|
-
char: "lexical-token-char",
|
|
80
|
-
class: "lexical-token-class",
|
|
81
|
-
comment: "lexical-token-comment",
|
|
82
|
-
constant: "lexical-token-constant",
|
|
83
|
-
deleted: "lexical-token-deleted",
|
|
84
|
-
doctype: "lexical-token-doctype",
|
|
85
|
-
entity: "lexical-token-entity",
|
|
86
|
-
function: "lexical-token-function",
|
|
87
|
-
important: "lexical-token-important",
|
|
88
|
-
inserted: "lexical-token-inserted",
|
|
89
|
-
keyword: "lexical-token-keyword",
|
|
90
|
-
namespace: "lexical-token-namespace",
|
|
91
|
-
number: "lexical-token-number",
|
|
92
|
-
operator: "lexical-token-operator",
|
|
93
|
-
prolog: "lexical-token-prolog",
|
|
94
|
-
property: "lexical-token-property",
|
|
95
|
-
punctuation: "lexical-token-punctuation",
|
|
96
|
-
regex: "lexical-token-regex",
|
|
97
|
-
selector: "lexical-token-selector",
|
|
98
|
-
string: "lexical-token-string",
|
|
99
|
-
symbol: "lexical-token-symbol",
|
|
100
|
-
tag: "lexical-token-tag",
|
|
101
|
-
url: "lexical-token-url",
|
|
102
|
-
variable: "lexical-token-variable"
|
|
103
|
-
},
|
|
94
|
+
codeHighlight: CODE_TOKEN_THEME,
|
|
104
95
|
link: "lexical-link",
|
|
105
96
|
text: {
|
|
106
97
|
bold: "lexical-text-bold",
|
|
@@ -108,7 +99,18 @@ const theme = {
|
|
|
108
99
|
underline: "lexical-text-underline",
|
|
109
100
|
strikethrough: "lexical-text-strike",
|
|
110
101
|
code: "lexical-text-code"
|
|
111
|
-
}
|
|
102
|
+
},
|
|
103
|
+
table: "lexical-table",
|
|
104
|
+
tableScrollableWrapper: "lexical-table-wrapper",
|
|
105
|
+
tableRow: "lexical-table-row",
|
|
106
|
+
tableCell: "lexical-table-cell",
|
|
107
|
+
tableCellHeader: "lexical-table-cell-header",
|
|
108
|
+
tableSelected: "lexical-table-selected",
|
|
109
|
+
tableSelection: "lexical-table-selection",
|
|
110
|
+
tableAddRows: "lexical-table-add-rows",
|
|
111
|
+
tableAddColumns: "lexical-table-add-columns",
|
|
112
|
+
tableDeleteRows: "lexical-table-delete-rows",
|
|
113
|
+
tableDeleteColumns: "lexical-table-delete-columns"
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
function Placeholder({ text }) {
|
|
@@ -120,48 +122,14 @@ function InitialContentPlugin({ html }) {
|
|
|
120
122
|
const [editor] = useLexicalComposerContext()
|
|
121
123
|
const lastApplied = useRef(null)
|
|
122
124
|
|
|
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
125
|
useEffect(() => {
|
|
161
126
|
if (lastApplied.current === html) return
|
|
162
127
|
lastApplied.current = html
|
|
163
128
|
editor.update(() => {
|
|
164
129
|
const root = $getRoot()
|
|
130
|
+
// Re-importing replaces the tree; drop stale resolved-language keys so the
|
|
131
|
+
// registry only tracks nodes from this import.
|
|
132
|
+
clearLanguageResolved(editor)
|
|
165
133
|
// Explicitly remove all children to ensure it's empty
|
|
166
134
|
root.getChildren().forEach((child) => child.remove())
|
|
167
135
|
|
|
@@ -170,10 +138,43 @@ function InitialContentPlugin({ html }) {
|
|
|
170
138
|
// No more .trix-content wrapper
|
|
171
139
|
const container = doc.body
|
|
172
140
|
|
|
141
|
+
// @lexical/code's importer only reads `data-language`, but the language is
|
|
142
|
+
// encoded differently depending on which renderer produced this HTML:
|
|
143
|
+
// commonmarker (server reopen) uses `<pre lang>`, while renderMarkdown (the
|
|
144
|
+
// markdown→rich toggle) puts it on `<pre><code class="language-X">`. Bridge
|
|
145
|
+
// both onto `data-language` so an explicit fence language survives reopen
|
|
146
|
+
// instead of being dropped (and then defaulted to javascript). Detection
|
|
147
|
+
// still corrects unlabeled blocks.
|
|
148
|
+
bridgeCodeFenceLanguages(container)
|
|
149
|
+
|
|
150
|
+
// Color / background-color are bound to text nodes during import by the
|
|
151
|
+
// colorAwareSpanImport html config (see lib/lexical/color_import). We no
|
|
152
|
+
// longer re-apply styles positionally after import, which used to drift
|
|
153
|
+
// onto the wrong text node whenever Lexical split or dropped text nodes.
|
|
173
154
|
syncLexicalStyleAttributes(container)
|
|
174
|
-
|
|
155
|
+
// Push color/background-color from non-span elements onto spans so the
|
|
156
|
+
// colorAwareSpanImport config binds it (the span importer can't see it
|
|
157
|
+
// otherwise). Must run after the sync above materializes data-lexical-*.
|
|
158
|
+
normalizeColoredContainers(container)
|
|
175
159
|
const nodes = $generateNodesFromDOM(editor, container)
|
|
176
160
|
|
|
161
|
+
// Mark code blocks whose language came from an explicit source label as
|
|
162
|
+
// resolved BEFORE registerCodeHighlighting bakes the "javascript" default
|
|
163
|
+
// onto unlabeled ones. At this point a non-empty language can only be one
|
|
164
|
+
// the bridge set from a real fence/attribute, so the detection transform
|
|
165
|
+
// will honor it verbatim (incl. an explicit "javascript") and only
|
|
166
|
+
// re-detect the still-unlabeled blocks.
|
|
167
|
+
const markExplicitCodeLanguages = (list) => {
|
|
168
|
+
list.forEach((node) => {
|
|
169
|
+
if ($isCodeNode(node)) {
|
|
170
|
+
if (node.getLanguage()) markLanguageResolved(editor, node.getKey())
|
|
171
|
+
} else if ($isElementNode(node) && typeof node.getChildren === "function") {
|
|
172
|
+
markExplicitCodeLanguages(node.getChildren())
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
markExplicitCodeLanguages(nodes)
|
|
177
|
+
|
|
177
178
|
// Filter out duplicate image nodes if any
|
|
178
179
|
const uniqueNodes = []
|
|
179
180
|
const seenImages = new Set()
|
|
@@ -192,24 +193,35 @@ function InitialContentPlugin({ html }) {
|
|
|
192
193
|
})
|
|
193
194
|
|
|
194
195
|
const appendedNodes = []
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
196
|
+
// Text nodes and inline elements (links, etc.) cannot live directly under
|
|
197
|
+
// the root. Minimized HTML stores a single line without its <p> wrapper, so
|
|
198
|
+
// a line like "Hello <strong>World</strong>" re-imports as several
|
|
199
|
+
// top-level inline nodes — group consecutive ones back into one paragraph
|
|
200
|
+
// so the line is not split apart.
|
|
201
|
+
let pendingParagraph = null
|
|
202
|
+
const flushPending = () => {
|
|
203
|
+
if (pendingParagraph) {
|
|
204
|
+
root.append(pendingParagraph)
|
|
205
|
+
appendedNodes.push(pendingParagraph)
|
|
206
|
+
pendingParagraph = null
|
|
202
207
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
208
|
+
}
|
|
209
|
+
uniqueNodes.forEach((node) => {
|
|
210
|
+
const isInlineLeaf =
|
|
211
|
+
$isTextNode(node) ||
|
|
212
|
+
$isLineBreakNode(node) ||
|
|
213
|
+
($isElementNode(node) && node.isInline())
|
|
214
|
+
if (isInlineLeaf) {
|
|
215
|
+
if (!pendingParagraph) pendingParagraph = $createParagraphNode()
|
|
216
|
+
pendingParagraph.append(node)
|
|
207
217
|
return
|
|
208
218
|
}
|
|
209
219
|
|
|
220
|
+
flushPending()
|
|
210
221
|
root.append(node)
|
|
211
222
|
appendedNodes.push(node)
|
|
212
223
|
})
|
|
224
|
+
flushPending()
|
|
213
225
|
|
|
214
226
|
if (root.getChildrenSize() === 0) {
|
|
215
227
|
const paragraph = $createParagraphNode()
|
|
@@ -217,12 +229,6 @@ function InitialContentPlugin({ html }) {
|
|
|
217
229
|
appendedNodes.push(paragraph)
|
|
218
230
|
}
|
|
219
231
|
|
|
220
|
-
const textNodes = root.getAllTextNodes()
|
|
221
|
-
textNodes.forEach((textNode, index) => {
|
|
222
|
-
const style = collectedStyles[index]
|
|
223
|
-
textNode.setStyle(style || "")
|
|
224
|
-
})
|
|
225
|
-
|
|
226
232
|
let lastChild = root.getLastChild()
|
|
227
233
|
while (
|
|
228
234
|
lastChild &&
|
|
@@ -236,12 +242,34 @@ function InitialContentPlugin({ html }) {
|
|
|
236
242
|
if (root.getChildrenSize() === 0) {
|
|
237
243
|
root.append($createParagraphNode())
|
|
238
244
|
}
|
|
245
|
+
|
|
246
|
+
// A blank line the user typed is an empty paragraph, but the server renders
|
|
247
|
+
// it as a standalone <br> that re-imports as a paragraph holding a
|
|
248
|
+
// LineBreakNode — which Lexical draws as TWO lines, so blank lines grew by
|
|
249
|
+
// one on every reopen. Split those marker paragraphs back into empty
|
|
250
|
+
// paragraphs so reopened blank lines match freshly typed ones. Runs AFTER
|
|
251
|
+
// the trailing-empty cleanup above so an intentional trailing blank line
|
|
252
|
+
// (a marker paragraph the cleanup leaves alone) is preserved, not stripped.
|
|
253
|
+
splitBlankLineParagraphs(root)
|
|
254
|
+
|
|
255
|
+
// A trailing top-level image/video/attachment leaves no caret position
|
|
256
|
+
// after it, making the document uneditable. Guarantee an editable
|
|
257
|
+
// paragraph after it. (The RootNode transform below keeps this true while
|
|
258
|
+
// editing; this handles the initial load deterministically regardless of
|
|
259
|
+
// plugin effect ordering.)
|
|
260
|
+
ensureTrailingParagraph(root)
|
|
239
261
|
})
|
|
240
|
-
}, [
|
|
262
|
+
}, [editor, html])
|
|
241
263
|
|
|
242
264
|
return null
|
|
243
265
|
}
|
|
244
266
|
|
|
267
|
+
function TrailingParagraphPlugin() {
|
|
268
|
+
const [editor] = useLexicalComposerContext()
|
|
269
|
+
useEffect(() => registerTrailingParagraph(editor), [editor])
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
|
|
245
273
|
function LinkAttributesPlugin() {
|
|
246
274
|
const [editor] = useLexicalComposerContext()
|
|
247
275
|
|
|
@@ -268,7 +296,34 @@ function CodeHighlightingPlugin() {
|
|
|
268
296
|
const [editor] = useLexicalComposerContext()
|
|
269
297
|
|
|
270
298
|
useEffect(() => {
|
|
271
|
-
|
|
299
|
+
const unregisterHighlight = registerCodeHighlighting(editor)
|
|
300
|
+
|
|
301
|
+
// registerCodeHighlighting bakes "javascript" onto any code block without a
|
|
302
|
+
// language (its tokenizer default), which then serializes into the canonical
|
|
303
|
+
// markdown as ```javascript — so Ruby/Python/etc. blocks get permanently
|
|
304
|
+
// mislabeled on the first edit. This transform re-detects the real language
|
|
305
|
+
// from the block's content whenever it's unconfirmed (missing or stuck on
|
|
306
|
+
// the javascript default) and corrects the node, so the editor shows — and
|
|
307
|
+
// saves — the right language. An explicit non-default language is left alone.
|
|
308
|
+
const unregisterDetect = editor.registerNodeTransform(CodeNode, (node) => {
|
|
309
|
+
// A language that came from an explicit source label on import is honored
|
|
310
|
+
// verbatim — including "javascript" — so auto-detection never overrides a
|
|
311
|
+
// deliberate choice. Only unlabeled/new blocks (baked to the javascript
|
|
312
|
+
// default) are re-detected from their content.
|
|
313
|
+
if (isLanguageResolved(editor, node.getKey())) return
|
|
314
|
+
const current = node.getLanguage()
|
|
315
|
+
const norm = normalizeFenceLang(current)
|
|
316
|
+
if (norm && norm !== "javascript") return
|
|
317
|
+
const detected = detectCodeLanguage(node.getTextContent(), current)
|
|
318
|
+
if (detected && detected !== "javascript" && detected !== current) {
|
|
319
|
+
node.setLanguage(detected)
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
return () => {
|
|
324
|
+
unregisterHighlight()
|
|
325
|
+
unregisterDetect()
|
|
326
|
+
}
|
|
272
327
|
}, [editor])
|
|
273
328
|
|
|
274
329
|
return null
|
|
@@ -495,6 +550,14 @@ function Toolbar() {
|
|
|
495
550
|
[editor]
|
|
496
551
|
)
|
|
497
552
|
|
|
553
|
+
const insertTable = useCallback(() => {
|
|
554
|
+
editor.dispatchCommand(INSERT_TABLE_COMMAND, {
|
|
555
|
+
columns: "3",
|
|
556
|
+
rows: "3",
|
|
557
|
+
includeHeaders: true
|
|
558
|
+
})
|
|
559
|
+
}, [editor])
|
|
560
|
+
|
|
498
561
|
const toggleLink = useCallback(() => {
|
|
499
562
|
let hasLink = false
|
|
500
563
|
let selectionText = ""
|
|
@@ -733,6 +796,14 @@ function Toolbar() {
|
|
|
733
796
|
title="Numbered list">
|
|
734
797
|
1.
|
|
735
798
|
</button>
|
|
799
|
+
<button
|
|
800
|
+
type="button"
|
|
801
|
+
className="lexical-toolbar-btn"
|
|
802
|
+
onClick={insertTable}
|
|
803
|
+
title="Insert table"
|
|
804
|
+
aria-label="Insert table">
|
|
805
|
+
▦
|
|
806
|
+
</button>
|
|
736
807
|
<span className="lexical-toolbar-separator" aria-hidden="true" />
|
|
737
808
|
<button
|
|
738
809
|
type="button"
|
|
@@ -893,6 +964,16 @@ function EditorInner({
|
|
|
893
964
|
}) {
|
|
894
965
|
const [editor] = useLexicalComposerContext()
|
|
895
966
|
|
|
967
|
+
// Anchor for the floating table plugins (hover "+" and the cell action menu).
|
|
968
|
+
// Portaling into the editor's own subtree (not document.body) ties the floating
|
|
969
|
+
// UI's lifetime to the editor DOM: when the editor is torn down — including
|
|
970
|
+
// Turbo/host teardown that removes the container without a React unmount — the
|
|
971
|
+
// chevron button is removed with it instead of being orphaned in document.body.
|
|
972
|
+
const [floatingAnchorElem, setFloatingAnchorElem] = useState(null)
|
|
973
|
+
const onAnchorRef = useCallback((el) => {
|
|
974
|
+
if (el !== null) setFloatingAnchorElem(el)
|
|
975
|
+
}, [])
|
|
976
|
+
|
|
896
977
|
// File drop is handled by FileUploadPlugin's DROP_COMMAND handler.
|
|
897
978
|
// We only need dragOver to allow the browser to accept file drops.
|
|
898
979
|
const handleDragOver = useCallback((event) => {
|
|
@@ -904,7 +985,7 @@ function EditorInner({
|
|
|
904
985
|
return (
|
|
905
986
|
<div className="lexical-editor-shell">
|
|
906
987
|
<Toolbar />
|
|
907
|
-
<div className="lexical-editor-inner">
|
|
988
|
+
<div className="lexical-editor-inner" ref={onAnchorRef}>
|
|
908
989
|
<RichTextPlugin
|
|
909
990
|
contentEditable={
|
|
910
991
|
<ContentEditable
|
|
@@ -922,12 +1003,18 @@ function EditorInner({
|
|
|
922
1003
|
<HistoryPlugin />
|
|
923
1004
|
<CodeHighlightingPlugin />
|
|
924
1005
|
<ListPlugin />
|
|
1006
|
+
<TablePlugin hasCellMerge={false} hasCellBackgroundColor={false} />
|
|
1007
|
+
{floatingAnchorElem && (
|
|
1008
|
+
<TableHoverActionsPlugin anchorElem={floatingAnchorElem} />
|
|
1009
|
+
)}
|
|
1010
|
+
<ListTabIndentPlugin />
|
|
925
1011
|
<LinkPlugin />
|
|
926
1012
|
<AutoLinkPlugin matchers={URL_MATCHERS} />
|
|
927
1013
|
<OnChangePlugin
|
|
928
1014
|
onChange={(editorState, editorInstance) => {
|
|
929
1015
|
if (!onChange) return
|
|
930
1016
|
let serialized = ""
|
|
1017
|
+
let markdown = ""
|
|
931
1018
|
editorState.read(() => {
|
|
932
1019
|
const innerHtml = $generateHtmlFromNodes(editorInstance)
|
|
933
1020
|
const parser = new DOMParser()
|
|
@@ -942,12 +1029,20 @@ function EditorInner({
|
|
|
942
1029
|
anchor.setAttribute("target", "_blank")
|
|
943
1030
|
anchor.setAttribute("rel", "noopener")
|
|
944
1031
|
})
|
|
945
|
-
|
|
1032
|
+
// Strip Lexical's verbose markup (extra <div>, white-space spans,
|
|
1033
|
+
// duplicate format wrappers, single-line <p>) before persisting.
|
|
1034
|
+
serialized = minimizeContentHtml(doc.body.firstElementChild)
|
|
1035
|
+
// Canonical Markdown projection (color/bg -> normalized <span>).
|
|
1036
|
+
// normalizeMarkdownBlankLines keeps the standard `\n\n` paragraph
|
|
1037
|
+
// separation (Enter = real paragraph break) and only tidies blank-
|
|
1038
|
+
// line runs / the empty-state — no marker characters in the source.
|
|
1039
|
+
markdown = normalizeMarkdownBlankLines($convertToMarkdownString(MARKDOWN_TRANSFORMERS))
|
|
946
1040
|
})
|
|
947
|
-
//
|
|
948
|
-
onChange(serialized)
|
|
1041
|
+
// html: client-side preview/fallback; markdown: canonical storage.
|
|
1042
|
+
onChange({ html: serialized, markdown })
|
|
949
1043
|
}}
|
|
950
1044
|
/>
|
|
1045
|
+
<TrailingParagraphPlugin />
|
|
951
1046
|
<InitialContentPlugin html={initialHtml} />
|
|
952
1047
|
<LinkAttributesPlugin />
|
|
953
1048
|
<ReadyPlugin onReady={onReady} />
|
|
@@ -1019,12 +1114,17 @@ export default function InlineLexicalEditor({
|
|
|
1019
1114
|
LinkNode,
|
|
1020
1115
|
AutoLinkNode,
|
|
1021
1116
|
ImageNode,
|
|
1022
|
-
AttachmentNode
|
|
1117
|
+
AttachmentNode,
|
|
1118
|
+
VideoNode,
|
|
1119
|
+
TableNode,
|
|
1120
|
+
TableRowNode,
|
|
1121
|
+
TableCellNode
|
|
1023
1122
|
],
|
|
1024
1123
|
onError(error) {
|
|
1025
1124
|
throw error
|
|
1026
1125
|
},
|
|
1027
|
-
theme
|
|
1126
|
+
theme,
|
|
1127
|
+
html: lexicalHtmlConfig
|
|
1028
1128
|
}),
|
|
1029
1129
|
[]
|
|
1030
1130
|
)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { LitElement, html, svg, nothing } from "lit";
|
|
2
|
-
import DOMPurify from "dompurify";
|
|
3
2
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
4
3
|
import { parseEmojis } from "../utils/emoji_parser";
|
|
4
|
+
import { highlightCodeBlocks } from "../lib/utils/markdown";
|
|
5
|
+
import { addCreativeTableDownloadButtons } from "../lib/utils/table_download";
|
|
6
|
+
import { sanitizeDescriptionHtml } from "../lib/utils/sanitize_description";
|
|
5
7
|
import csrfFetch from "../lib/api/csrf_fetch";
|
|
6
8
|
|
|
7
9
|
const BULLET_STARTING_LEVEL = 3;
|
|
@@ -78,6 +80,19 @@ class CreativeTreeRow extends LitElement {
|
|
|
78
80
|
updated(changedProperties) {
|
|
79
81
|
this._attachHandlers();
|
|
80
82
|
|
|
83
|
+
// Re-tokenize the server-rendered description code blocks with hljs so they
|
|
84
|
+
// match the editor's palette and follow light/dark theme. Idempotent: only
|
|
85
|
+
// unmarked blocks are processed, and lit re-renders description DOM (dropping
|
|
86
|
+
// the marker) only when descriptionHtml actually changes.
|
|
87
|
+
highlightCodeBlocks(this);
|
|
88
|
+
|
|
89
|
+
// Attach CSV/Excel download toolbars to markdown tables in the description
|
|
90
|
+
// display areas so creative tables match the chat-message table UI.
|
|
91
|
+
// Scoped to .creative-content/.creative-title-content (not the whole row)
|
|
92
|
+
// so the inline editor's own tables are never wrapped. Idempotent, safe on
|
|
93
|
+
// every Lit re-render.
|
|
94
|
+
addCreativeTableDownloadButtons(this);
|
|
95
|
+
|
|
81
96
|
if (changedProperties.has('loadingChildren')) {
|
|
82
97
|
if (this.loadingChildren) {
|
|
83
98
|
this._startAnimation();
|
|
@@ -99,8 +114,10 @@ class CreativeTreeRow extends LitElement {
|
|
|
99
114
|
|
|
100
115
|
set descriptionHtml(value) {
|
|
101
116
|
const oldValue = this._descriptionHtml;
|
|
102
|
-
// Always sanitize when setting new HTML
|
|
103
|
-
|
|
117
|
+
// Always sanitize when setting new HTML. Uses the shared sanitizer so the
|
|
118
|
+
// server-generated YouTube preview iframe survives (default DOMPurify config
|
|
119
|
+
// strips all iframes, which is what broke the YouTube link preview).
|
|
120
|
+
const sanitized = sanitizeDescriptionHtml(value);
|
|
104
121
|
this._descriptionHtml = sanitized;
|
|
105
122
|
this.dataset.descriptionHtml = sanitized;
|
|
106
123
|
this.requestUpdate("descriptionHtml", oldValue);
|
|
@@ -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,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useEffect } from "react"
|
|
2
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
|
|
3
|
+
import { registerListTabIndentation } from "../../lib/lexical/list_tab_indent"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Enables Tab / Shift+Tab nesting for list items in the inline editor.
|
|
7
|
+
* The actual command handling lives in registerListTabIndentation so it can be
|
|
8
|
+
* unit-tested against a headless editor.
|
|
9
|
+
*/
|
|
10
|
+
export default function ListTabIndentPlugin() {
|
|
11
|
+
const [editor] = useLexicalComposerContext()
|
|
12
|
+
|
|
13
|
+
useEffect(() => registerListTabIndentation(editor), [editor])
|
|
14
|
+
|
|
15
|
+
return null
|
|
16
|
+
}
|