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

EcrituresNumeriques / stylo / 15066714766

16 May 2025 10:47AM UTC coverage: 37.574% (-0.02%) from 37.594%
15066714766

push

github

web-flow
Mise à jour vers react-router@7 (#1517)

Co-authored-by: Thomas Parisot <thom4parisot@users.noreply.github.com>
Co-authored-by: Guillaume Grossetie <ggrossetie@yuzutech.fr>

549 of 776 branches covered (70.75%)

Branch coverage included in aggregate %.

13 of 373 new or added lines in 28 files covered. (3.49%)

5 existing lines in 4 files now uncovered.

5319 of 14841 relevant lines covered (35.84%)

2.56 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 throttle from 'lodash.throttle'
×
4
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
×
5
import { MonacoBinding } from 'y-monaco'
×
6

×
7
import { useArticleVersion, useEditableArticle } from '../../hooks/article.js'
×
8
import { useStyloExportPreview } from '../../hooks/stylo-export.js'
×
9
import { useBibliographyCompletion } from '../../hooks/bibliography.js'
×
10
import { useCollaboration } from '../../hooks/collaboration.js'
×
11

×
12
import CollaborativeEditorWebSocketStatus from './CollaborativeEditorWebSocketStatus.jsx'
×
13
import Alert from '../molecules/Alert.jsx'
×
14
import Loading from '../molecules/Loading.jsx'
×
15
import defaultEditorOptions from '../Write/providers/monaco/options.js'
×
16

×
17
import styles from './CollaborativeTextEditor.module.scss'
×
18
import MonacoEditor from '../molecules/MonacoEditor.jsx'
×
NEW
19
import { DiffEditor } from '@monaco-editor/react'
×
20
import * as vscode from 'monaco-editor'
×
21
import { onDropIntoEditor } from '../Write/providers/monaco/support.js'
×
22

×
23
/**
×
24
 * @param {object} props
×
25
 * @param {string} props.articleId
×
26
 * @param {string|undefined} props.versionId
×
27
 * @param {'write' | 'compare' | 'preview'} props.mode
×
28
 * @returns {Element}
×
29
 */
×
30
export default function CollaborativeTextEditor({
×
31
  articleId,
×
32
  versionId,
×
33
  mode,
×
34
}) {
×
35
  const { yText, awareness, websocketStatus, dynamicStyles } = useCollaboration(
×
36
    { articleId, versionId }
×
37
  )
×
38

×
NEW
39
  const { version, error, isLoading: isVersionLoading } = useArticleVersion({ versionId })
×
40
  const { provider: bibliographyCompletionProvider } =
×
41
    useBibliographyCompletion()
×
NEW
42
  const { article, bibliography, isLoading: isWorkingVersionLoading } = useEditableArticle({
×
43
    articleId,
×
44
    versionId,
×
45
  })
×
46

×
47
  const { html: __html, isLoading: isPreviewLoading } = useStyloExportPreview({
×
48
    ...(mode === 'preview'
×
49
      ? {
×
50
          md_content: versionId ? version.md : yText?.toString(),
×
51
          yaml_content: versionId
×
52
            ? version.yaml
×
53
            : article?.workingVersion?.yaml,
×
54
          bib_content: versionId ? version.bib : article?.workingVersion?.bib,
×
55
        }
×
56
      : {}),
×
57
    with_toc: true,
×
58
    with_nocite: true,
×
59
    with_link_citations: true,
×
60
  })
×
61

×
62
  const dispatch = useDispatch()
×
63
  const editorRef = useRef(null)
×
64
  const editorCursorPosition = useSelector(
×
65
    (state) => state.editorCursorPosition,
×
66
    shallowEqual
×
67
  )
×
68

×
69
  const hasVersion = useMemo(() => !!versionId, [versionId])
×
NEW
70
  const isLoading = yText === null || isPreviewLoading || isWorkingVersionLoading || isVersionLoading
×
71

×
72
  const options = useMemo(
×
73
    () => ({
×
74
      ...defaultEditorOptions,
×
75
      contextmenu: hasVersion ? false : websocketStatus === 'connected',
×
76
      readOnly: hasVersion ? true : websocketStatus !== 'connected',
×
77
      dropIntoEditor: {
×
78
        enabled: true,
×
79
      },
×
80
    }),
×
81
    [websocketStatus, hasVersion]
×
82
  )
×
83

×
NEW
84
  const updateArticleStructureAndStats = useCallback(throttle(
×
NEW
85
    ({ text: md }) => {
×
NEW
86
      dispatch({ type: 'UPDATE_ARTICLE_STATS', md })
×
NEW
87
      dispatch({ type: 'UPDATE_ARTICLE_STRUCTURE', md })
×
88
    },
×
89
    250,
×
90
    { leading: false, trailing: true }
×
NEW
91
  ), [])
×
92

×
93
  const handleCollaborativeEditorDidMount = useCallback(
×
94
    (editor, monaco) => {
×
95
      editorRef.current = editor
×
96
      editor.onDropIntoEditor(onDropIntoEditor(editor))
×
97
      const completionProvider = bibliographyCompletionProvider.register(monaco)
×
98
      editor.onDidDispose(() => completionProvider.dispose())
×
99
      const model = editor.getModel()
×
100
      // Set EOL to LF otherwise it causes synchronization issues due to inconsistent EOL between Windows and Linux.
×
101
      // https://github.com/yjs/y-monaco/issues/27
×
102
      model.setEOL(monaco.editor.EndOfLineSequence.LF)
×
103
      if (yText && awareness) {
×
104
        new MonacoBinding(yText, model, new Set([editor]), awareness)
×
105
      }
×
106
    },
×
107
    [yText, awareness]
×
108
  )
×
109

×
110
  const handleEditorDidMount = useCallback((editor) => {
×
111
    editorRef.current = editor
×
112
  }, [])
×
113

×
114
  let timeoutId
×
115
  useEffect(() => {
×
116
    if (yText) {
×
117
      yText.observe(function (yTextEvent, transaction) {
×
118
        dispatch({
×
119
          type: 'UPDATE_ARTICLE_WORKING_COPY_STATUS',
×
120
          status: 'syncing',
×
121
        })
×
122
        if (timeoutId) {
×
123
          clearTimeout(timeoutId)
×
124
        }
×
125
        timeoutId = setTimeout(() => {
×
126
          dispatch({
×
127
            type: 'UPDATE_ARTICLE_WORKING_COPY_STATUS',
×
128
            status: 'synced',
×
129
          })
×
130
        }, 4000)
×
131

×
NEW
132
        updateArticleStructureAndStats({ text: yText.toString() })
×
133
      })
×
134
    }
×
NEW
135
  }, [articleId, versionId, yText])
×
136

×
137
  useEffect(() => {
×
NEW
138
    if (versionId) {
×
139
      dispatch({ type: 'UPDATE_ARTICLE_STATS', md: version.md })
×
140
      dispatch({ type: 'UPDATE_ARTICLE_STRUCTURE', md: version.md })
×
141
    }
×
NEW
142
  }, [versionId])
×
143

×
144
  useEffect(() => {
×
145
    if (bibliography) {
×
146
      bibliographyCompletionProvider.bibTeXEntries = bibliography.entries
×
147
    }
×
148
  }, [bibliography])
×
149

×
150
  useEffect(() => {
×
151
    const line = editorCursorPosition.lineNumber
×
152
    const editor = editorRef.current
×
153
    editor?.focus()
×
154
    const endOfLineColumn = editor?.getModel()?.getLineMaxColumn(line + 1)
×
155
    editor?.setPosition({ lineNumber: line + 1, column: endOfLineColumn })
×
156
    editor?.revealLineNearTop(line + 1, 1) // smooth
×
157
  }, [editorRef, editorCursorPosition])
×
158

×
NEW
159
  if (isLoading) {
×
160
    return <Loading />
×
161
  }
×
162

×
163
  if (error) {
×
164
    return <Alert message={error.message} />
×
165
  }
×
166

×
167
  return (
×
168
    <>
×
169
      <style>{dynamicStyles}</style>
×
170
      <Helmet>
×
171
        <title>{article.title}</title>
×
172
      </Helmet>
×
173

×
174
      <CollaborativeEditorWebSocketStatus
×
175
        className={styles.inlineStatus}
×
176
        status={websocketStatus}
×
177
      />
×
178

×
179
      {mode === 'preview' && (
×
180
        <section
×
181
          className={styles.previewPage}
×
182
          dangerouslySetInnerHTML={{ __html }}
×
183
        />
×
184
      )}
×
185

×
NEW
186
      {mode === 'compare' && (
×
NEW
187
        <div className={styles.collaborativeEditor}>
×
NEW
188
          <DiffEditor
×
NEW
189
            className={styles.editor}
×
NEW
190
            width={'100%'}
×
NEW
191
            height={'auto'}
×
NEW
192
            modified={article.workingVersion?.md}
×
NEW
193
            original={version.md}
×
NEW
194
            language="markdown"
×
NEW
195
            options={defaultEditorOptions}
×
NEW
196
          />
×
NEW
197
        </div>
×
NEW
198
      )}
×
NEW
199

×
NEW
200
      <div className={styles.collaborativeEditor} hidden={mode !== 'write'}>
×
201
        <MonacoEditor
×
202
          width={'100%'}
×
203
          height={'auto'}
×
204
          options={options}
×
205
          className={styles.editor}
×
206
          defaultLanguage="markdown"
×
NEW
207
          {...(hasVersion
×
NEW
208
            ? { value: version.md, onMount: handleEditorDidMount }
×
NEW
209
            : { onMount: handleCollaborativeEditorDidMount })}
×
210
        />
×
211
      </div>
×
212
    </>
×
213
  )
×
214
}
×
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