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

EcrituresNumeriques / stylo / 17724634656

15 Sep 2025 06:54AM UTC coverage: 39.426% (+0.04%) from 39.389%
17724634656

push

github

web-flow
fix: Utilise le titre de l'article dans le composant entête (#1668)

586 of 824 branches covered (71.12%)

Branch coverage included in aggregate %.

0 of 9 new or added lines in 3 files covered. (0.0%)

2 existing lines in 1 file now uncovered.

5786 of 15338 relevant lines covered (37.72%)

2.61 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 { ack } from './actions'
×
8
import { DiffEditor } from '@monaco-editor/react'
×
9
import throttle from 'lodash.throttle'
×
10

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

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

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

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

43
  const {
×
44
    version,
×
45
    error,
×
46
    isLoading: isVersionLoading,
×
47
  } = useArticleVersion({ versionId })
×
48
  const { provider: bibliographyCompletionProvider } =
×
49
    useBibliographyCompletion()
×
50
  const {
×
51
    article,
×
52
    bibliography,
×
53
    isLoading: isWorkingVersionLoading,
×
54
  } = useEditableArticle({
×
55
    articleId,
×
56
    versionId,
×
57
  })
×
58

59
  const { html: __html, isLoading: isPreviewLoading } = useStyloExportPreview({
×
60
    ...(mode === 'preview'
×
61
      ? {
×
62
          md_content: versionId ? version.md : yText?.toString(),
×
63
          yaml_content: versionId
×
64
            ? version.yaml
×
65
            : article?.workingVersion?.yaml,
×
66
          bib_content: versionId ? version.bib : article?.workingVersion?.bib,
×
67
        }
×
68
      : {}),
×
69
    with_toc: true,
×
70
    with_nocite: true,
×
71
    with_link_citations: true,
×
72
  })
×
73

74
  const dispatch = useDispatch()
×
75
  const editorRef = useRef(null)
×
76
  const editorCursorPosition = useSelector(
×
77
    (state) => state.editorCursorPosition,
×
78
    shallowEqual
×
79
  )
×
80

81
  const hasVersion = useMemo(() => !!versionId, [versionId])
×
82
  const isLoading =
×
83
    yText === null ||
×
84
    isPreviewLoading ||
×
85
    isWorkingVersionLoading ||
×
86
    isVersionLoading
×
87

88
  const options = useMemo(
×
89
    () => ({
×
90
      ...defaultEditorOptions,
×
91
      contextmenu: hasVersion ? false : websocketStatus === 'connected',
×
92
      readOnly: hasVersion ? true : websocketStatus !== 'connected',
×
93
      dropIntoEditor: {
×
94
        enabled: true,
×
95
      },
×
96
    }),
×
97
    [websocketStatus, hasVersion]
×
98
  )
×
99

100
  const updateArticleStructureAndStats = useCallback(
×
101
    throttle(
×
102
      ({ text: md }) => {
×
103
        dispatch({ type: 'UPDATE_ARTICLE_STATS', md })
×
104
        dispatch({ type: 'UPDATE_ARTICLE_STRUCTURE', md })
×
105
      },
×
106
      250,
×
107
      { leading: false, trailing: true }
×
108
    ),
×
109
    []
×
110
  )
×
111

112
  const handleCollaborativeEditorDidMount = useCallback(
×
113
    (editor, monaco) => {
×
114
      editorRef.current = editor
×
115

116
      editor.onDropIntoEditor(onDropIntoEditor(editor))
×
117

118
      // Action commands
119
      editor.addAction({ ...ack, label: t(ack.label) })
×
120

121
      const completionProvider = bibliographyCompletionProvider.register(monaco)
×
122
      editor.onDidDispose(() => completionProvider.dispose())
×
123

124
      const model = editor.getModel()
×
125
      // Set EOL to LF otherwise it causes synchronization issues due to inconsistent EOL between Windows and Linux.
126
      // https://github.com/yjs/y-monaco/issues/27
127
      model.setEOL(monaco.editor.EndOfLineSequence.LF)
×
128
      if (yText && awareness) {
×
129
        new MonacoBinding(yText, model, new Set([editor]), awareness)
×
130
      }
×
131
    },
×
132
    [yText, awareness]
×
133
  )
×
134

135
  const handleEditorDidMount = useCallback((editor) => {
×
136
    editorRef.current = editor
×
137
  }, [])
×
138

139
  let timeoutId
×
140
  useEffect(() => {
×
141
    if (yText) {
×
142
      updateArticleStructureAndStats({ text: yText.toString() })
×
143
      yText.observe(function (yTextEvent, transaction) {
×
144
        dispatch({
×
145
          type: 'UPDATE_ARTICLE_WORKING_COPY_STATUS',
×
146
          status: 'syncing',
×
147
        })
×
148
        if (timeoutId) {
×
149
          clearTimeout(timeoutId)
×
150
        }
×
151
        timeoutId = setTimeout(() => {
×
152
          dispatch({
×
153
            type: 'UPDATE_ARTICLE_WORKING_COPY_STATUS',
×
154
            status: 'synced',
×
155
          })
×
156
        }, 4000)
×
157

158
        updateArticleStructureAndStats({ text: yText.toString() })
×
159
      })
×
160
    }
×
161
  }, [articleId, versionId, yText])
×
162

163
  useEffect(() => {
×
164
    if (versionId) {
×
165
      dispatch({ type: 'UPDATE_ARTICLE_STATS', md: version.md })
×
166
      dispatch({ type: 'UPDATE_ARTICLE_STRUCTURE', md: version.md })
×
167
    }
×
168
  }, [versionId])
×
169

170
  useEffect(() => {
×
171
    if (bibliography) {
×
172
      bibliographyCompletionProvider.bibTeXEntries = bibliography.entries
×
173
    }
×
174
  }, [bibliography])
×
175

176
  useEffect(() => {
×
177
    const line = editorCursorPosition.lineNumber
×
178
    const editor = editorRef.current
×
179
    editor?.focus()
×
180
    const endOfLineColumn = editor?.getModel()?.getLineMaxColumn(line + 1)
×
181
    editor?.setPosition({ lineNumber: line + 1, column: endOfLineColumn })
×
182
    editor?.revealLineNearTop(line + 1, 1) // smooth
×
183
  }, [editorRef, editorCursorPosition])
×
184

185
  if (isLoading) {
×
186
    return <Loading />
×
187
  }
×
188

189
  if (error) {
×
190
    return <Alert message={error.message} />
×
191
  }
×
192

193
  return (
×
194
    <>
×
195
      <style>{dynamicStyles}</style>
×
196
      <Helmet>
×
197
        <title>{article.title}</title>
×
198
      </Helmet>
×
199

NEW
200
      <CollaborativeEditorArticleHeader
×
NEW
201
        articleTitle={article.title}
×
NEW
202
        versionId={versionId}
×
NEW
203
      />
×
204

205
      <CollaborativeEditorWebSocketStatus
×
206
        className={styles.inlineStatus}
×
207
        status={websocketStatus}
×
208
      />
×
209

210
      {mode === 'preview' && (
×
211
        <section
×
212
          className={styles.previewPage}
×
213
          dangerouslySetInnerHTML={{ __html }}
×
214
        />
×
215
      )}
216

217
      {mode === 'compare' && (
×
218
        <div className={styles.collaborativeEditor}>
×
219
          <DiffEditor
×
220
            className={styles.editor}
×
221
            width={'100%'}
×
222
            height={'auto'}
×
223
            modified={article.workingVersion?.md}
×
224
            original={version.md}
×
225
            language="markdown"
×
226
            options={defaultEditorOptions}
×
227
          />
×
228
        </div>
×
229
      )}
230

231
      <div className={styles.collaborativeEditor} hidden={mode !== 'write'}>
×
232
        <MonacoEditor
×
233
          width={'100%'}
×
234
          height={'auto'}
×
235
          options={options}
×
236
          className={styles.editor}
×
237
          defaultLanguage="markdown"
×
238
          {...(hasVersion
×
239
            ? { value: version.md, onMount: handleEditorDidMount }
×
240
            : { onMount: handleCollaborativeEditorDidMount })}
×
241
        />
×
242
      </div>
×
243
    </>
×
244
  )
245
}
×
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