collavre 0.22.0 → 0.24.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/images/collavre/landing/interface-en-dark.png +0 -0
- data/app/assets/images/collavre/landing/interface-en.png +0 -0
- data/app/assets/images/collavre/landing/interface-ko-dark.png +0 -0
- data/app/assets/images/collavre/landing/interface-ko.png +0 -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 +158 -61
- data/app/assets/stylesheets/collavre/creatives.css +11 -2
- data/app/assets/stylesheets/collavre/landing.css +92 -0
- data/app/assets/stylesheets/collavre/modal_dialog.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +4 -2
- data/app/assets/stylesheets/collavre/tables.css +91 -0
- data/app/channels/collavre/inbox_badge_channel.rb +30 -0
- data/app/controllers/collavre/admin/settings_controller.rb +52 -67
- data/app/controllers/collavre/api/v1/agents_controller.rb +17 -188
- data/app/controllers/collavre/api/v1/base_controller.rb +9 -27
- 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 +13 -3
- data/app/controllers/collavre/concerns/slide_viewable.rb +8 -32
- data/app/controllers/collavre/creatives_controller.rb +47 -103
- data/app/controllers/collavre/inbox_items_controller.rb +10 -4
- data/app/controllers/collavre/landing_controller.rb +5 -2
- data/app/controllers/collavre/tasks_controller.rb +13 -4
- data/app/controllers/collavre/topics_controller.rb +54 -1
- data/app/controllers/collavre/typo_corrections_controller.rb +39 -0
- data/app/controllers/concerns/collavre/api/authenticatable.rb +40 -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/errors/collavre/api_error.rb +20 -0
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/helpers/collavre/creatives_helper.rb +30 -8
- data/app/javascript/collavre.js +2 -0
- data/app/javascript/components/ImageResizer.jsx +9 -3
- data/app/javascript/components/InlineLexicalEditor.jsx +157 -72
- data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +87 -0
- data/app/javascript/components/creative_tree_row.js +28 -5
- data/app/javascript/components/plugins/list_tab_indent_plugin.jsx +16 -0
- data/app/javascript/components/plugins/markdown_shortcuts_plugin.jsx +15 -4
- 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__/search_popup_controller.test.js +104 -0
- data/app/javascript/controllers/__tests__/topic_list_controller.test.js +82 -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/__tests__/topics_controller_topic_list.test.js +61 -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 +61 -8
- data/app/javascript/controllers/common_popup_controller.js +2 -2
- 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 +7 -1
- data/app/javascript/controllers/search_popup_controller.js +17 -1
- data/app/javascript/controllers/share_modal_controller.js +4 -3
- data/app/javascript/controllers/topic_list_controller.js +78 -0
- 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__/common_popup_bounds.test.js +58 -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 +42 -10
- 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__/code_block_toggle.test.js +326 -0
- data/app/javascript/lib/lexical/__tests__/code_fence_shortcut.test.js +179 -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/code_block_toggle.js +130 -0
- data/app/javascript/lib/lexical/code_fence_shortcut.js +60 -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__/creative_inline_payload.test.js +231 -0
- data/app/javascript/modules/__tests__/creative_save_queue.test.js +228 -0
- data/app/javascript/modules/__tests__/creative_tree_dom.test.js +425 -0
- data/app/javascript/modules/__tests__/typo_corrector.test.js +365 -0
- data/app/javascript/modules/creative_inline_payload.js +86 -0
- data/app/javascript/modules/creative_row_editor.js +231 -471
- data/app/javascript/modules/creative_row_editor_helpers.js +76 -0
- data/app/javascript/modules/creative_save_queue.js +95 -0
- data/app/javascript/modules/creative_tree_dom.js +260 -0
- 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/jobs/collavre/permission_cache_job.rb +20 -2
- data/app/jobs/collavre/trigger_loop_check_job.rb +20 -3
- data/app/jobs/collavre/update_mcp_tools_job.rb +17 -0
- data/app/models/collavre/channel.rb +16 -0
- data/app/models/collavre/comment/approvable.rb +16 -0
- data/app/models/collavre/comment/broadcastable.rb +46 -7
- data/app/models/collavre/comment/notifiable.rb +15 -5
- data/app/models/collavre/comment.rb +87 -32
- data/app/models/collavre/comment_version.rb +1 -1
- data/app/models/collavre/creative/describable.rb +104 -19
- data/app/models/collavre/creative/linkable.rb +9 -0
- data/app/models/collavre/creative/permissible.rb +97 -49
- data/app/models/collavre/creative.rb +73 -5
- data/app/models/collavre/creative_share.rb +78 -26
- data/app/models/collavre/inbox_item.rb +2 -2
- data/app/models/collavre/task.rb +27 -0
- data/app/models/collavre/user.rb +90 -10
- data/app/models/concerns/collavre/indexed_json_columns.rb +58 -0
- data/app/services/collavre/agent_type_classifier.rb +23 -0
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +4 -0
- data/app/services/collavre/ai_agent/message_builder.rb +1 -1
- data/app/services/collavre/ai_agent/session_provisioner.rb +126 -0
- data/app/services/collavre/ai_agent/task_claim_service.rb +96 -0
- data/app/services/collavre/ai_client.rb +81 -12
- data/app/services/collavre/auto_theme_generator.rb +15 -11
- data/app/services/collavre/creatives/children_index.rb +86 -0
- data/app/services/collavre/creatives/comment_badge_index.rb +73 -0
- data/app/services/collavre/creatives/creative_tree_serializer.rb +122 -0
- data/app/services/collavre/creatives/effective_creative_resolution.rb +37 -0
- data/app/services/collavre/creatives/index_query.rb +85 -16
- data/app/services/collavre/creatives/permission_checker.rb +1 -1
- data/app/services/collavre/creatives/permission_filter.rb +144 -17
- data/app/services/collavre/creatives/tree_builder.rb +107 -78
- data/app/services/collavre/gemini_parent_recommender.rb +1 -1
- data/app/services/collavre/http_client.rb +119 -0
- data/app/services/collavre/inbox_reply_service.rb +5 -0
- data/app/services/collavre/link_preview_fetcher.rb +129 -38
- data/app/services/collavre/markdown_converter.rb +13 -3
- data/app/services/collavre/markdown_importer.rb +30 -6
- data/app/services/collavre/mobile/event_summarizer.rb +40 -0
- data/app/services/collavre/orchestration/agent_context_builder.rb +1 -15
- data/app/services/collavre/orchestration/agent_orchestrator.rb +34 -8
- 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/system_events/context_builder.rb +1 -14
- 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/tools/preview_attach_service.rb +5 -7
- data/app/services/collavre/typo_corrector.rb +188 -0
- data/app/views/collavre/comments/_comment.html.erb +9 -4
- data/app/views/collavre/comments/_comments_popup.html.erb +25 -4
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +10 -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/landing/show.html.erb +52 -10
- 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 +16 -0
- data/config/locales/comments.ko.yml +16 -0
- data/config/locales/creatives.en.yml +6 -0
- data/config/locales/creatives.ko.yml +5 -0
- data/config/locales/integrations.en.yml +1 -1
- data/config/locales/integrations.ko.yml +1 -1
- data/config/locales/landing.en.yml +22 -10
- data/config/locales/landing.ko.yml +22 -10
- 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/20260120045354_encrypt_oauth_tokens.rb +13 -4
- 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/migrate/20260702000001_enforce_system_settings_key_not_null.rb +9 -0
- data/db/migrate/20260702000002_add_creative_index_and_fk_to_tags.rb +11 -0
- data/db/migrate/20260702000003_add_creative_type_index_to_labels.rb +10 -0
- data/db/migrate/20260702000004_add_creative_created_at_index_to_comments.rb +10 -0
- data/db/migrate/20260702000005_promote_channel_config_index_columns.rb +76 -0
- data/db/migrate/20260713000001_add_comments_count_to_creatives.rb +20 -0
- data/db/migrate/20260713000002_add_comment_versions_count_to_comments.rb +20 -0
- data/db/migrate/20260715000000_change_creatives_description_to_plain_text.rb +16 -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 +96 -2
- data/app/services/collavre/tools/description_normalizable.rb +0 -16
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEditor,
|
|
3
|
+
$getRoot,
|
|
4
|
+
$createParagraphNode,
|
|
5
|
+
$createTextNode,
|
|
6
|
+
$createRangeSelection,
|
|
7
|
+
$setSelection,
|
|
8
|
+
KEY_ENTER_COMMAND
|
|
9
|
+
} from "lexical"
|
|
10
|
+
import { HeadingNode, QuoteNode, registerRichText } from "@lexical/rich-text"
|
|
11
|
+
import { CodeNode, CodeHighlightNode, $isCodeNode } from "@lexical/code"
|
|
12
|
+
import { registerCodeFenceShortcut } from "../code_fence_shortcut"
|
|
13
|
+
import { isLanguageResolved } from "../../editor/code_languages"
|
|
14
|
+
|
|
15
|
+
function buildEditor() {
|
|
16
|
+
const editor = createEditor({
|
|
17
|
+
namespace: "test",
|
|
18
|
+
onError(error) {
|
|
19
|
+
throw error
|
|
20
|
+
},
|
|
21
|
+
nodes: [HeadingNode, QuoteNode, CodeNode, CodeHighlightNode]
|
|
22
|
+
})
|
|
23
|
+
registerRichText(editor)
|
|
24
|
+
registerCodeFenceShortcut(editor)
|
|
25
|
+
return editor
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Put `text` into a fresh paragraph with the caret at its end — the state right
|
|
29
|
+
// before the user presses Enter. This does NOT convert on its own; conversion is
|
|
30
|
+
// the Enter keystroke, so the language token can be typed first.
|
|
31
|
+
function setFenceLine(editor, text) {
|
|
32
|
+
editor.update(
|
|
33
|
+
() => {
|
|
34
|
+
const root = $getRoot()
|
|
35
|
+
root.clear()
|
|
36
|
+
const paragraph = $createParagraphNode()
|
|
37
|
+
const textNode = $createTextNode(text)
|
|
38
|
+
paragraph.append(textNode)
|
|
39
|
+
root.append(paragraph)
|
|
40
|
+
const selection = $createRangeSelection()
|
|
41
|
+
selection.anchor.set(textNode.getKey(), text.length, "text")
|
|
42
|
+
selection.focus.set(textNode.getKey(), text.length, "text")
|
|
43
|
+
$setSelection(selection)
|
|
44
|
+
},
|
|
45
|
+
{ discrete: true }
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Dispatch a bare Enter (the fence-commit keystroke). Returns nothing; read the
|
|
50
|
+
// editor state afterwards to assert the outcome.
|
|
51
|
+
function pressEnter(editor, { shiftKey = false } = {}) {
|
|
52
|
+
editor.dispatchCommand(KEY_ENTER_COMMAND, {
|
|
53
|
+
shiftKey,
|
|
54
|
+
preventDefault() {}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("registerCodeFenceShortcut", () => {
|
|
59
|
+
it("does not convert until Enter — so ```lang can be typed first", () => {
|
|
60
|
+
const editor = buildEditor()
|
|
61
|
+
// The fence line is present with the caret at its end, but no Enter yet.
|
|
62
|
+
setFenceLine(editor, "```")
|
|
63
|
+
editor.read(() => {
|
|
64
|
+
expect($isCodeNode($getRoot().getFirstChild())).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Extending the fence with a language must not have converted it either.
|
|
68
|
+
setFenceLine(editor, "```json")
|
|
69
|
+
editor.read(() => {
|
|
70
|
+
expect($isCodeNode($getRoot().getFirstChild())).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("converts a bare ``` fence into an empty code block on Enter", () => {
|
|
75
|
+
const editor = buildEditor()
|
|
76
|
+
setFenceLine(editor, "```")
|
|
77
|
+
pressEnter(editor)
|
|
78
|
+
|
|
79
|
+
editor.read(() => {
|
|
80
|
+
const children = $getRoot().getChildren()
|
|
81
|
+
expect(children).toHaveLength(1)
|
|
82
|
+
expect($isCodeNode(children[0])).toBe(true)
|
|
83
|
+
expect(children[0].getTextContent()).toBe("")
|
|
84
|
+
expect(children[0].getLanguage()).toBeFalsy()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("carries the language token from ```json on Enter", () => {
|
|
89
|
+
const editor = buildEditor()
|
|
90
|
+
setFenceLine(editor, "```json")
|
|
91
|
+
pressEnter(editor)
|
|
92
|
+
|
|
93
|
+
editor.read(() => {
|
|
94
|
+
const codeNode = $getRoot().getFirstChild()
|
|
95
|
+
expect($isCodeNode(codeNode)).toBe(true)
|
|
96
|
+
expect(codeNode.getLanguage()).toBe("json")
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("marks an explicit fence language resolved so highlighting won't relabel it", () => {
|
|
101
|
+
const editor = buildEditor()
|
|
102
|
+
// ```javascript is ambiguous with the tokenizer's baked default; without the
|
|
103
|
+
// resolved marker the detect transform would re-detect and could relabel it.
|
|
104
|
+
setFenceLine(editor, "```javascript")
|
|
105
|
+
pressEnter(editor)
|
|
106
|
+
|
|
107
|
+
let key
|
|
108
|
+
editor.read(() => {
|
|
109
|
+
const codeNode = $getRoot().getFirstChild()
|
|
110
|
+
expect($isCodeNode(codeNode)).toBe(true)
|
|
111
|
+
expect(codeNode.getLanguage()).toBe("javascript")
|
|
112
|
+
key = codeNode.getKey()
|
|
113
|
+
})
|
|
114
|
+
expect(isLanguageResolved(editor, key)).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("does not mark a bare ``` fence resolved", () => {
|
|
118
|
+
const editor = buildEditor()
|
|
119
|
+
setFenceLine(editor, "```")
|
|
120
|
+
pressEnter(editor)
|
|
121
|
+
|
|
122
|
+
let key
|
|
123
|
+
editor.read(() => {
|
|
124
|
+
key = $getRoot().getFirstChild().getKey()
|
|
125
|
+
})
|
|
126
|
+
expect(isLanguageResolved(editor, key)).toBe(false)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("leaves Shift+Enter as a soft newline, not a fence commit", () => {
|
|
130
|
+
const editor = buildEditor()
|
|
131
|
+
setFenceLine(editor, "```")
|
|
132
|
+
pressEnter(editor, { shiftKey: true })
|
|
133
|
+
|
|
134
|
+
editor.read(() => {
|
|
135
|
+
expect($isCodeNode($getRoot().getFirstChild())).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it("leaves prose that merely contains backticks untouched on Enter", () => {
|
|
140
|
+
const editor = buildEditor()
|
|
141
|
+
setFenceLine(editor, "run ```code``` inline")
|
|
142
|
+
pressEnter(editor)
|
|
143
|
+
|
|
144
|
+
editor.read(() => {
|
|
145
|
+
const child = $getRoot().getFirstChild()
|
|
146
|
+
expect($isCodeNode(child)).toBe(false)
|
|
147
|
+
expect(child.getTextContent()).toBe("run ```code``` inline")
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("does not convert a ``` paragraph when the caret is elsewhere", () => {
|
|
152
|
+
const editor = buildEditor()
|
|
153
|
+
editor.update(
|
|
154
|
+
() => {
|
|
155
|
+
const root = $getRoot()
|
|
156
|
+
root.clear()
|
|
157
|
+
const fence = $createParagraphNode()
|
|
158
|
+
fence.append($createTextNode("```"))
|
|
159
|
+
root.append(fence)
|
|
160
|
+
const other = $createParagraphNode()
|
|
161
|
+
const otherText = $createTextNode("elsewhere")
|
|
162
|
+
other.append(otherText)
|
|
163
|
+
root.append(other)
|
|
164
|
+
// Caret sits in the OTHER paragraph, not the fence line.
|
|
165
|
+
const selection = $createRangeSelection()
|
|
166
|
+
selection.anchor.set(otherText.getKey(), 1, "text")
|
|
167
|
+
selection.focus.set(otherText.getKey(), 1, "text")
|
|
168
|
+
$setSelection(selection)
|
|
169
|
+
},
|
|
170
|
+
{ discrete: true }
|
|
171
|
+
)
|
|
172
|
+
pressEnter(editor)
|
|
173
|
+
|
|
174
|
+
editor.read(() => {
|
|
175
|
+
const children = $getRoot().getChildren()
|
|
176
|
+
expect(children.some($isCodeNode)).toBe(false)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*
|
|
4
|
+
* Regression for "an external image steals editing focus": clicking into the
|
|
5
|
+
* text to edit while an image is present used to wipe the caret. ImageResizer
|
|
6
|
+
* registered a CLICK_COMMAND handler that called the node-selection hook's
|
|
7
|
+
* setSelected(false) on any click outside the image. That hook, when the live
|
|
8
|
+
* selection is NOT already a NodeSelection (i.e. it is the RangeSelection caret
|
|
9
|
+
* the click just placed), synthesizes a fresh empty NodeSelection and replaces
|
|
10
|
+
* the caret with it — so the editor lost its caret and became uneditable the
|
|
11
|
+
* instant you clicked to edit. The fix uses clearSelection(), which only acts on
|
|
12
|
+
* an existing NodeSelection and leaves a RangeSelection (the caret) intact.
|
|
13
|
+
*
|
|
14
|
+
* ImageResizer is a .jsx React component that can't be imported under this
|
|
15
|
+
* native-ESM jest config, so this test reconstructs the two deselect paths and
|
|
16
|
+
* the exact CLICK_COMMAND handler logic on a headless editor.
|
|
17
|
+
*/
|
|
18
|
+
import {
|
|
19
|
+
createEditor,
|
|
20
|
+
$getRoot,
|
|
21
|
+
$getSelection,
|
|
22
|
+
$isRangeSelection,
|
|
23
|
+
$isNodeSelection,
|
|
24
|
+
$createNodeSelection,
|
|
25
|
+
$setSelection,
|
|
26
|
+
$createParagraphNode,
|
|
27
|
+
$createTextNode,
|
|
28
|
+
$applyNodeReplacement,
|
|
29
|
+
DecoratorNode,
|
|
30
|
+
CLICK_COMMAND,
|
|
31
|
+
COMMAND_PRIORITY_LOW,
|
|
32
|
+
} from "lexical"
|
|
33
|
+
|
|
34
|
+
class TestImageNode extends DecoratorNode {
|
|
35
|
+
__src
|
|
36
|
+
static getType() { return "image" }
|
|
37
|
+
static clone(n) { return new TestImageNode(n.__src, n.__key) }
|
|
38
|
+
static importJSON() { return $createImage("") }
|
|
39
|
+
constructor(src = "", key) { super(key); this.__src = src }
|
|
40
|
+
createDOM() { return document.createElement("span") }
|
|
41
|
+
updateDOM() { return false }
|
|
42
|
+
decorate() { return null }
|
|
43
|
+
}
|
|
44
|
+
function $createImage(src) { return $applyNodeReplacement(new TestImageNode(src)) }
|
|
45
|
+
|
|
46
|
+
// Build: [image][paragraph "hello"] with the caret placed inside the text,
|
|
47
|
+
// i.e. the state right after a user clicks into the paragraph to edit.
|
|
48
|
+
function setup() {
|
|
49
|
+
const editor = createEditor({ namespace: "t", nodes: [TestImageNode], onError: (e) => { throw e } })
|
|
50
|
+
// jsdom: createEditor needs a root element to dispatch DOM-bound commands.
|
|
51
|
+
const el = document.createElement("div")
|
|
52
|
+
el.contentEditable = "true"
|
|
53
|
+
document.body.appendChild(el)
|
|
54
|
+
editor.setRootElement(el)
|
|
55
|
+
let imageKey
|
|
56
|
+
editor.update(() => {
|
|
57
|
+
const root = $getRoot()
|
|
58
|
+
root.clear()
|
|
59
|
+
root.append($createImage("https://lipcoding.kr/x.png"))
|
|
60
|
+
const p = $createParagraphNode()
|
|
61
|
+
p.append($createTextNode("hello"))
|
|
62
|
+
root.append(p)
|
|
63
|
+
imageKey = root.getFirstChild().getKey()
|
|
64
|
+
}, { discrete: true })
|
|
65
|
+
editor.update(() => {
|
|
66
|
+
$getRoot().getLastChild().getFirstChild().select(2, 2)
|
|
67
|
+
}, { discrete: true })
|
|
68
|
+
return { editor, imageKey }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function selKind(editor) {
|
|
72
|
+
let kind = "none"
|
|
73
|
+
editor.getEditorState().read(() => {
|
|
74
|
+
const s = $getSelection()
|
|
75
|
+
if ($isRangeSelection(s)) kind = "range"
|
|
76
|
+
else if ($isNodeSelection(s)) kind = "node"
|
|
77
|
+
})
|
|
78
|
+
return kind
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Mirror of the hook's setSelected(false): the OLD, destructive deselect path.
|
|
82
|
+
function setSelectedFalse(editor, key) {
|
|
83
|
+
editor.update(() => {
|
|
84
|
+
let selection = $getSelection()
|
|
85
|
+
if (!$isNodeSelection(selection)) {
|
|
86
|
+
selection = $createNodeSelection()
|
|
87
|
+
$setSelection(selection)
|
|
88
|
+
}
|
|
89
|
+
if ($isNodeSelection(selection)) selection.delete(key)
|
|
90
|
+
}, { discrete: true })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Mirror of the hook's clearSelection(): the safe deselect path the fix uses.
|
|
94
|
+
function clearSelection(editor) {
|
|
95
|
+
editor.update(() => {
|
|
96
|
+
const selection = $getSelection()
|
|
97
|
+
if ($isNodeSelection(selection)) selection.clear()
|
|
98
|
+
}, { discrete: true })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe("image editing focus", () => {
|
|
102
|
+
test("the old setSelected(false) deselect destroys the caret (documents the bug)", () => {
|
|
103
|
+
const { editor, imageKey } = setup()
|
|
104
|
+
expect(selKind(editor)).toBe("range")
|
|
105
|
+
setSelectedFalse(editor, imageKey)
|
|
106
|
+
expect(selKind(editor)).not.toBe("range") // caret lost — the reported bug
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test("clearSelection() leaves the caret intact", () => {
|
|
110
|
+
const { editor } = setup()
|
|
111
|
+
expect(selKind(editor)).toBe("range")
|
|
112
|
+
clearSelection(editor)
|
|
113
|
+
expect(selKind(editor)).toBe("range")
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("CLICK_COMMAND outside the image keeps the caret (fixed handler)", () => {
|
|
117
|
+
const { editor } = setup()
|
|
118
|
+
|
|
119
|
+
// Stand-in for ImageResizer's container ref; the click target is text
|
|
120
|
+
// outside it, exactly like clicking into the paragraph to edit.
|
|
121
|
+
const container = document.createElement("span")
|
|
122
|
+
const outsideTarget = document.createElement("span")
|
|
123
|
+
|
|
124
|
+
editor.registerCommand(
|
|
125
|
+
CLICK_COMMAND,
|
|
126
|
+
(event) => {
|
|
127
|
+
if (container && !container.contains(event.target)) {
|
|
128
|
+
clearSelection(editor) // fixed path (was setSelected(false))
|
|
129
|
+
}
|
|
130
|
+
return false
|
|
131
|
+
},
|
|
132
|
+
COMMAND_PRIORITY_LOW
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
expect(selKind(editor)).toBe("range")
|
|
136
|
+
editor.dispatchCommand(CLICK_COMMAND, { target: outsideTarget })
|
|
137
|
+
expect(selKind(editor)).toBe("range") // caret survives the click
|
|
138
|
+
})
|
|
139
|
+
})
|