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.
- 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 +118 -61
- data/app/assets/stylesheets/collavre/creatives.css +11 -2
- data/app/assets/stylesheets/collavre/modal_dialog.css +32 -0
- data/app/assets/stylesheets/collavre/tables.css +91 -0
- data/app/channels/collavre/inbox_badge_channel.rb +30 -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/creatives_controller.rb +16 -5
- data/app/controllers/collavre/tasks_controller.rb +13 -4
- data/app/controllers/collavre/topics_controller.rb +49 -1
- data/app/controllers/collavre/typo_corrections_controller.rb +39 -0
- 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 +155 -38
- data/app/javascript/components/creative_tree_row.js +20 -3
- 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/comment_controller.js +5 -4
- 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 +3 -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 +18 -17
- data/app/javascript/controllers/comments/presence_controller.js +2 -1
- 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/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/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__/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 +20 -1
- 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/list_tab_indent.js +210 -0
- data/app/javascript/lib/lexical/markdown_serialize.js +320 -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/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 +7 -4
- data/app/jobs/collavre/compress_job.rb +6 -2
- data/app/models/collavre/comment/broadcastable.rb +46 -7
- data/app/models/collavre/comment/notifiable.rb +14 -4
- data/app/models/collavre/comment.rb +79 -31
- data/app/models/collavre/creative/describable.rb +89 -10
- data/app/models/collavre/task.rb +15 -0
- data/app/models/collavre/user.rb +57 -1
- data/app/services/collavre/ai_client.rb +28 -10
- data/app/services/collavre/auto_theme_generator.rb +1 -1
- data/app/services/collavre/creatives/index_query.rb +85 -16
- data/app/services/collavre/creatives/tree_builder.rb +2 -1
- 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 +141 -34
- 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_update_service.rb +23 -8
- data/app/services/collavre/typo_corrector.rb +188 -0
- data/app/views/collavre/comments/_comment.html.erb +5 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +14 -1
- 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/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/comments.en.yml +15 -0
- data/config/locales/comments.ko.yml +15 -0
- data/config/locales/integrations.en.yml +1 -1
- data/config/locales/integrations.ko.yml +1 -1
- 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 +13 -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/version.rb +1 -1
- data/lib/generators/collavre/install/install_generator.rb +1 -0
- metadata +55 -2
- data/app/services/collavre/tools/description_normalizable.rb +0 -16
|
@@ -21,6 +21,9 @@ 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,
|
|
@@ -51,10 +54,20 @@ import { AttachmentNode } from "../lib/lexical/attachment_node"
|
|
|
51
54
|
import { VideoNode } from "../lib/lexical/video_node"
|
|
52
55
|
import AttachmentCleanupPlugin from "./plugins/attachment_cleanup_plugin"
|
|
53
56
|
import MarkdownShortcutsPlugin from "./plugins/markdown_shortcuts_plugin"
|
|
57
|
+
import ListTabIndentPlugin from "./plugins/list_tab_indent_plugin"
|
|
54
58
|
import { syncLexicalStyleAttributes } from "../lib/lexical/style_attributes"
|
|
55
59
|
import { lexicalHtmlConfig, normalizeColoredContainers } from "../lib/lexical/color_import"
|
|
56
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"
|
|
57
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"
|
|
58
71
|
|
|
59
72
|
const URL_MATCHERS = [
|
|
60
73
|
createLinkMatcherWithRegExp(/https?:\/\/[^\s<]+/gi, (text) => text)
|
|
@@ -71,40 +84,14 @@ const theme = {
|
|
|
71
84
|
list: {
|
|
72
85
|
ul: "lexical-list-ul",
|
|
73
86
|
ol: "lexical-list-ol",
|
|
74
|
-
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" }
|
|
75
92
|
},
|
|
76
93
|
code: "lexical-code-block",
|
|
77
|
-
codeHighlight:
|
|
78
|
-
atrule: "lexical-token-atrule",
|
|
79
|
-
attr: "lexical-token-attr",
|
|
80
|
-
boolean: "lexical-token-boolean",
|
|
81
|
-
builtin: "lexical-token-builtin",
|
|
82
|
-
cdata: "lexical-token-cdata",
|
|
83
|
-
char: "lexical-token-char",
|
|
84
|
-
class: "lexical-token-class",
|
|
85
|
-
comment: "lexical-token-comment",
|
|
86
|
-
constant: "lexical-token-constant",
|
|
87
|
-
deleted: "lexical-token-deleted",
|
|
88
|
-
doctype: "lexical-token-doctype",
|
|
89
|
-
entity: "lexical-token-entity",
|
|
90
|
-
function: "lexical-token-function",
|
|
91
|
-
important: "lexical-token-important",
|
|
92
|
-
inserted: "lexical-token-inserted",
|
|
93
|
-
keyword: "lexical-token-keyword",
|
|
94
|
-
namespace: "lexical-token-namespace",
|
|
95
|
-
number: "lexical-token-number",
|
|
96
|
-
operator: "lexical-token-operator",
|
|
97
|
-
prolog: "lexical-token-prolog",
|
|
98
|
-
property: "lexical-token-property",
|
|
99
|
-
punctuation: "lexical-token-punctuation",
|
|
100
|
-
regex: "lexical-token-regex",
|
|
101
|
-
selector: "lexical-token-selector",
|
|
102
|
-
string: "lexical-token-string",
|
|
103
|
-
symbol: "lexical-token-symbol",
|
|
104
|
-
tag: "lexical-token-tag",
|
|
105
|
-
url: "lexical-token-url",
|
|
106
|
-
variable: "lexical-token-variable"
|
|
107
|
-
},
|
|
94
|
+
codeHighlight: CODE_TOKEN_THEME,
|
|
108
95
|
link: "lexical-link",
|
|
109
96
|
text: {
|
|
110
97
|
bold: "lexical-text-bold",
|
|
@@ -112,7 +99,18 @@ const theme = {
|
|
|
112
99
|
underline: "lexical-text-underline",
|
|
113
100
|
strikethrough: "lexical-text-strike",
|
|
114
101
|
code: "lexical-text-code"
|
|
115
|
-
}
|
|
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"
|
|
116
114
|
}
|
|
117
115
|
|
|
118
116
|
function Placeholder({ text }) {
|
|
@@ -129,6 +127,9 @@ function InitialContentPlugin({ html }) {
|
|
|
129
127
|
lastApplied.current = html
|
|
130
128
|
editor.update(() => {
|
|
131
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)
|
|
132
133
|
// Explicitly remove all children to ensure it's empty
|
|
133
134
|
root.getChildren().forEach((child) => child.remove())
|
|
134
135
|
|
|
@@ -137,6 +138,15 @@ function InitialContentPlugin({ html }) {
|
|
|
137
138
|
// No more .trix-content wrapper
|
|
138
139
|
const container = doc.body
|
|
139
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
|
+
|
|
140
150
|
// Color / background-color are bound to text nodes during import by the
|
|
141
151
|
// colorAwareSpanImport html config (see lib/lexical/color_import). We no
|
|
142
152
|
// longer re-apply styles positionally after import, which used to drift
|
|
@@ -148,6 +158,23 @@ function InitialContentPlugin({ html }) {
|
|
|
148
158
|
normalizeColoredContainers(container)
|
|
149
159
|
const nodes = $generateNodesFromDOM(editor, container)
|
|
150
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
|
+
|
|
151
178
|
// Filter out duplicate image nodes if any
|
|
152
179
|
const uniqueNodes = []
|
|
153
180
|
const seenImages = new Set()
|
|
@@ -215,12 +242,34 @@ function InitialContentPlugin({ html }) {
|
|
|
215
242
|
if (root.getChildrenSize() === 0) {
|
|
216
243
|
root.append($createParagraphNode())
|
|
217
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)
|
|
218
261
|
})
|
|
219
262
|
}, [editor, html])
|
|
220
263
|
|
|
221
264
|
return null
|
|
222
265
|
}
|
|
223
266
|
|
|
267
|
+
function TrailingParagraphPlugin() {
|
|
268
|
+
const [editor] = useLexicalComposerContext()
|
|
269
|
+
useEffect(() => registerTrailingParagraph(editor), [editor])
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
|
|
224
273
|
function LinkAttributesPlugin() {
|
|
225
274
|
const [editor] = useLexicalComposerContext()
|
|
226
275
|
|
|
@@ -247,7 +296,34 @@ function CodeHighlightingPlugin() {
|
|
|
247
296
|
const [editor] = useLexicalComposerContext()
|
|
248
297
|
|
|
249
298
|
useEffect(() => {
|
|
250
|
-
|
|
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
|
+
}
|
|
251
327
|
}, [editor])
|
|
252
328
|
|
|
253
329
|
return null
|
|
@@ -474,6 +550,14 @@ function Toolbar() {
|
|
|
474
550
|
[editor]
|
|
475
551
|
)
|
|
476
552
|
|
|
553
|
+
const insertTable = useCallback(() => {
|
|
554
|
+
editor.dispatchCommand(INSERT_TABLE_COMMAND, {
|
|
555
|
+
columns: "3",
|
|
556
|
+
rows: "3",
|
|
557
|
+
includeHeaders: true
|
|
558
|
+
})
|
|
559
|
+
}, [editor])
|
|
560
|
+
|
|
477
561
|
const toggleLink = useCallback(() => {
|
|
478
562
|
let hasLink = false
|
|
479
563
|
let selectionText = ""
|
|
@@ -712,6 +796,14 @@ function Toolbar() {
|
|
|
712
796
|
title="Numbered list">
|
|
713
797
|
1.
|
|
714
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>
|
|
715
807
|
<span className="lexical-toolbar-separator" aria-hidden="true" />
|
|
716
808
|
<button
|
|
717
809
|
type="button"
|
|
@@ -872,6 +964,16 @@ function EditorInner({
|
|
|
872
964
|
}) {
|
|
873
965
|
const [editor] = useLexicalComposerContext()
|
|
874
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
|
+
|
|
875
977
|
// File drop is handled by FileUploadPlugin's DROP_COMMAND handler.
|
|
876
978
|
// We only need dragOver to allow the browser to accept file drops.
|
|
877
979
|
const handleDragOver = useCallback((event) => {
|
|
@@ -883,7 +985,7 @@ function EditorInner({
|
|
|
883
985
|
return (
|
|
884
986
|
<div className="lexical-editor-shell">
|
|
885
987
|
<Toolbar />
|
|
886
|
-
<div className="lexical-editor-inner">
|
|
988
|
+
<div className="lexical-editor-inner" ref={onAnchorRef}>
|
|
887
989
|
<RichTextPlugin
|
|
888
990
|
contentEditable={
|
|
889
991
|
<ContentEditable
|
|
@@ -901,12 +1003,18 @@ function EditorInner({
|
|
|
901
1003
|
<HistoryPlugin />
|
|
902
1004
|
<CodeHighlightingPlugin />
|
|
903
1005
|
<ListPlugin />
|
|
1006
|
+
<TablePlugin hasCellMerge={false} hasCellBackgroundColor={false} />
|
|
1007
|
+
{floatingAnchorElem && (
|
|
1008
|
+
<TableHoverActionsPlugin anchorElem={floatingAnchorElem} />
|
|
1009
|
+
)}
|
|
1010
|
+
<ListTabIndentPlugin />
|
|
904
1011
|
<LinkPlugin />
|
|
905
1012
|
<AutoLinkPlugin matchers={URL_MATCHERS} />
|
|
906
1013
|
<OnChangePlugin
|
|
907
1014
|
onChange={(editorState, editorInstance) => {
|
|
908
1015
|
if (!onChange) return
|
|
909
1016
|
let serialized = ""
|
|
1017
|
+
let markdown = ""
|
|
910
1018
|
editorState.read(() => {
|
|
911
1019
|
const innerHtml = $generateHtmlFromNodes(editorInstance)
|
|
912
1020
|
const parser = new DOMParser()
|
|
@@ -924,11 +1032,17 @@ function EditorInner({
|
|
|
924
1032
|
// Strip Lexical's verbose markup (extra <div>, white-space spans,
|
|
925
1033
|
// duplicate format wrappers, single-line <p>) before persisting.
|
|
926
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))
|
|
927
1040
|
})
|
|
928
|
-
//
|
|
929
|
-
onChange(serialized)
|
|
1041
|
+
// html: client-side preview/fallback; markdown: canonical storage.
|
|
1042
|
+
onChange({ html: serialized, markdown })
|
|
930
1043
|
}}
|
|
931
1044
|
/>
|
|
1045
|
+
<TrailingParagraphPlugin />
|
|
932
1046
|
<InitialContentPlugin html={initialHtml} />
|
|
933
1047
|
<LinkAttributesPlugin />
|
|
934
1048
|
<ReadyPlugin onReady={onReady} />
|
|
@@ -1001,7 +1115,10 @@ export default function InlineLexicalEditor({
|
|
|
1001
1115
|
AutoLinkNode,
|
|
1002
1116
|
ImageNode,
|
|
1003
1117
|
AttachmentNode,
|
|
1004
|
-
VideoNode
|
|
1118
|
+
VideoNode,
|
|
1119
|
+
TableNode,
|
|
1120
|
+
TableRowNode,
|
|
1121
|
+
TableCellNode
|
|
1005
1122
|
],
|
|
1006
1123
|
onError(error) {
|
|
1007
1124
|
throw error
|
|
@@ -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);
|
|
@@ -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
|
+
}
|