• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

EcrituresNumeriques / stylo / 18693266536

21 Oct 2025 06:06PM UTC coverage: 39.659% (-0.08%) from 39.742%
18693266536

push

github

web-flow
fix: ajoute un menu tout en conservant les autres actions Monaco (#1731)

Co-authored-by: Thomas Parisot <138627+thom4parisot@users.noreply.github.com>

613 of 860 branches covered (71.28%)

Branch coverage included in aggregate %.

59 of 116 new or added lines in 4 files covered. (50.86%)

40 existing lines in 7 files now uncovered.

6104 of 16077 relevant lines covered (37.97%)

2.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/front/src/components/collaborative/CollaborativeTextEditor.jsx
1
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
×
2
import { Helmet } from 'react-helmet'
×
3
import { useTranslation } from 'react-i18next'
×
4
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
×
5
import { MonacoBinding } from 'y-monaco'
×
6

NEW
7
import { MetopesMenu, actions, bindAction } from './actions'
×
8
import { DiffEditor } from '@monaco-editor/react'
×
9
import throttle from 'lodash.throttle'
×
NEW
10
import 'monaco-editor/esm/vs/base/browser/ui/codicons/codicon/codicon.css'
×
11

12
import { useArticleVersion, useEditableArticle } from '../../hooks/article.js'
×
13
import { useBibliographyCompletion } from '../../hooks/bibliography.js'
×
14
import { useCollaboration } from '../../hooks/collaboration.js'
×
15
import { useStyloExportPreview } from '../../hooks/stylo-export.js'
×
16
import defaultEditorOptions from '../Write/providers/monaco/options.js'
×
17
import { onDropIntoEditor } from '../Write/providers/monaco/support.js'
×
18

19
import Alert from '../molecules/Alert.jsx'
×
20
import Loading from '../molecules/Loading.jsx'
×
21
import MonacoEditor from '../molecules/MonacoEditor.jsx'
×
22
import CollaborativeEditorArticleHeader from './CollaborativeEditorArticleHeader.jsx'
×
23
import CollaborativeEditorWebSocketStatus from './CollaborativeEditorWebSocketStatus.jsx'
×
24

25
import styles from './CollaborativeTextEditor.module.scss'
×
26

27
/**
28
 * @typedef {import('monaco-editor').editor.IStandaloneCodeEditor} IStandaloneCodeEditor
29
 * @typedef {import('monaco-editor')} monaco
30
 */
31

32
/**
33
 * @param {object} props
34
 * @param {string} props.articleId
35
 * @param {string|undefined} props.versionId
36
 * @param {'write' | 'compare' | 'preview'} props.mode
37
 * @returns {Element}
38
 */
39
export default function CollaborativeTextEditor({
×
40
  articleId,
×
41
  versionId,
×
42
  mode,
×
43
}) {
×
44
  const { yText, awareness, websocketStatus, dynamicStyles } = useCollaboration(
×
45
    { articleId, versionId }
×
46
  )
×
47
  const { t } = useTranslation('editor')
×
48

49
  const {
×
50
    version,
×
51
    error,
×
52
    isLoading: isVersionLoading,
×
53
  } = useArticleVersion({ versionId })
×
54
  const { provider: bibliographyCompletionProvider } =
×
55
    useBibliographyCompletion()
×
56
  const {
×
57
    article,
×
58
    bibliography,
×
59
    isLoading: isWorkingVersionLoading,
×
60
  } = useEditableArticle({
×
61
    articleId,
×
62
    versionId,
×
63
  })
×
64

65
  const { html: __html, isLoading: isPreviewLoading } = useStyloExportPreview({
×
66
    ...(mode === 'preview'
×
67
      ? {
×
68
          md_content: versionId ? version.md : yText?.toString(),
×
69
          yaml_content: versionId
×
70
            ? version.yaml
×
71
            : article?.workingVersion?.yaml,
×
72
          bib_content: versionId ? version.bib : article?.workingVersion?.bib,
×
73
        }
×
74
      : {}),
×
75
    with_toc: true,
×
76
    with_nocite: true,
×
77
    with_link_citations: true,
×
78
  })
×
79

80
  const dispatch = useDispatch()
×
81
  const editorRef = useRef(null)
×
82
  const editorCursorPosition = useSelector(
×
83
    (state) => state.editorCursorPosition,
×
84
    shallowEqual
×
85
  )
×
86

87
  const hasVersion = useMemo(() => !!versionId, [versionId])
×
88
  const isLoading =
×
89
    yText === null ||
×
90
    isPreviewLoading ||
×
91
    isWorkingVersionLoading ||
×
92
    isVersionLoading
×
93

94
  const options = useMemo(
×
95
    () => ({
×
96
      ...defaultEditorOptions,
×
97
      contextmenu: hasVersion ? false : websocketStatus === 'connected',
×
98
      readOnly: hasVersion ? true : websocketStatus !== 'connected',
×
99
      dropIntoEditor: {
×
100
        enabled: true,
×
101
      },
×
102
    }),
×
103
    [websocketStatus, hasVersion]
×
104
  )
×
105

106
  const updateArticleStructureAndStats = useCallback(
×
107
    throttle(
×
108
      ({ text: md }) => {
×
109
        dispatch({ type: 'UPDATE_ARTICLE_STATS', md })
×
110
        dispatch({ type: 'UPDATE_ARTICLE_STRUCTURE', md })
×
111
      },
×
112
      250,
×
113
      { leading: false, trailing: true }
×
114
    ),
×
115
    []
×
116
  )
×
117

118
  const handleCollaborativeEditorDidMount = useCallback(
×
NEW
119
    (
×
NEW
120
      /** @type {IStandaloneCodeEditor} */ editor,
×
NEW
121
      /** @type {monaco} */ monaco
×
NEW
122
    ) => {
×
UNCOV
123
      editorRef.current = editor
×
124

125
      editor.onDropIntoEditor(onDropIntoEditor(editor))
×
126

NEW
127
      const contextMenu = editor.getContribution('editor.contrib.contextmenu')
×
NEW
128
      contextMenu._onContextMenu = function (e) {
×
UNCOV
129
        e.event.preventDefault()
×
NEW
130
        return contextMenu._contextMenuService.showContextMenu({
×
131
          getAnchor: () => ({
×
132
            x: e.event.browserEvent.pageX,
×
NEW
133
            y: e.event.browserEvent.pageY,
×
134
          }),
×
135
          getActions: () => [
×
136
            MetopesMenu({ editor, t }),
×
137
            // new Separator(),
138
          ],
×
139
        })
×
NEW
140
      }
×
141

142
      // Command Palette commands
143
      const _bindAction = bindAction.bind(null, editor, t)
×
144
      editor.addAction(_bindAction(actions.acknowledgement))
×
145

146
      const completionProvider = bibliographyCompletionProvider.register(monaco)
×
147
      editor.onDidDispose(() => completionProvider.dispose())
×
148

149
      const model = editor.getModel()
×
150
      // Set EOL to LF otherwise it causes synchronization issues due to inconsistent EOL between Windows and Linux.
151
      // https://github.com/yjs/y-monaco/issues/27
152
      model.setEOL(monaco.editor.EndOfLineSequence.LF)
×
153
      if (yText && awareness) {
×
154
        new MonacoBinding(yText, model, new Set([editor]), awareness)
×
155
      }
×
156
    },
×
157
    [yText, awareness]
×
158
  )
×
159

160
  const handleEditorDidMount = useCallback((editor) => {
×
161
    editorRef.current = editor
×
162
  }, [])
×
163

164
  let timeoutId
×
165
  useEffect(() => {
×
166
    if (yText) {
×
167
      updateArticleStructureAndStats({ text: yText.toString() })
×
168
      yText.observe(function (yTextEvent, transaction) {
×
169
        dispatch({
×
170
          type: 'UPDATE_ARTICLE_WORKING_COPY_STATUS',
×
171
          status: 'syncing',
×
172
        })
×
173
        if (timeoutId) {
×
174
          clearTimeout(timeoutId)
×
175
        }
×
176
        timeoutId = setTimeout(() => {
×
177
          dispatch({
×
178
            type: 'UPDATE_ARTICLE_WORKING_COPY_STATUS',
×
179
            status: 'synced',
×
180
          })
×
181
        }, 4000)
×
182

183
        updateArticleStructureAndStats({ text: yText.toString() })
×
184
      })
×
185
    }
×
186
  }, [articleId, versionId, yText])
×
187

188
  useEffect(() => {
×
189
    if (versionId) {
×
190
      dispatch({ type: 'UPDATE_ARTICLE_STATS', md: version.md })
×
191
      dispatch({ type: 'UPDATE_ARTICLE_STRUCTURE', md: version.md })
×
192
    }
×
193
  }, [versionId])
×
194

195
  useEffect(() => {
×
196
    if (bibliography) {
×
197
      bibliographyCompletionProvider.bibTeXEntries = bibliography.entries
×
198
    }
×
199
  }, [bibliography])
×
200

201
  useEffect(() => {
×
202
    const line = editorCursorPosition.lineNumber
×
203
    const editor = editorRef.current
×
204
    editor?.focus()
×
205
    const endOfLineColumn = editor?.getModel()?.getLineMaxColumn(line + 1)
×
206
    editor?.setPosition({ lineNumber: line + 1, column: endOfLineColumn })
×
207
    editor?.revealLineNearTop(line + 1, 1) // smooth
×
208
  }, [editorRef, editorCursorPosition])
×
209

210
  if (isLoading) {
×
211
    return <Loading />
×
212
  }
×
213

214
  if (error) {
×
215
    return <Alert message={error.message} />
×
216
  }
×
217

218
  return (
×
219
    <>
×
220
      <style>{dynamicStyles}</style>
×
221
      <Helmet>
×
222
        <title>{article.title}</title>
×
223
      </Helmet>
×
224

225
      <CollaborativeEditorArticleHeader
×
226
        articleTitle={article.title}
×
227
        versionId={versionId}
×
228
      />
×
229

230
      <CollaborativeEditorWebSocketStatus
×
231
        className={styles.inlineStatus}
×
232
        status={websocketStatus}
×
233
      />
×
234

235
      {mode === 'preview' && (
×
236
        <section
×
237
          className={styles.previewPage}
×
238
          dangerouslySetInnerHTML={{ __html }}
×
239
        />
×
240
      )}
241

242
      {mode === 'compare' && (
×
243
        <div className={styles.collaborativeEditor}>
×
244
          <DiffEditor
×
245
            className={styles.editor}
×
246
            width={'100%'}
×
247
            height={'auto'}
×
248
            modified={article.workingVersion?.md}
×
249
            original={version.md}
×
250
            language="markdown"
×
251
            options={defaultEditorOptions}
×
252
          />
×
253
        </div>
×
254
      )}
255

256
      <div className={styles.collaborativeEditor} hidden={mode !== 'write'}>
×
257
        <MonacoEditor
×
258
          width={'100%'}
×
259
          height={'auto'}
×
260
          options={options}
×
261
          className={styles.editor}
×
262
          defaultLanguage="markdown"
×
263
          {...(hasVersion
×
264
            ? { value: version.md, onMount: handleEditorDidMount }
×
265
            : { onMount: handleCollaborativeEditorDidMount })}
×
266
        />
×
267
      </div>
×
268
    </>
×
269
  )
270
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc