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

EcrituresNumeriques / stylo / 16348091781

17 Jul 2025 02:36PM UTC coverage: 39.148% (-0.08%) from 39.229%
16348091781

push

github

web-flow
feat(editor): ajout de bloc acknowledgment (#1664)

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

571 of 807 branches covered (70.76%)

Branch coverage included in aggregate %.

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

5704 of 15222 relevant lines covered (37.47%)

2.57 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'
×
NEW
3
import { useTranslation } from 'react-i18next'
×
4
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
×
5
import { MonacoBinding } from 'y-monaco'
×
6

NEW
7
import { DiffEditor } from '@monaco-editor/react'
×
NEW
8
import throttle from 'lodash.throttle'
×
9

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

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

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

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

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

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

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

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

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

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

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

115
      editor.onDropIntoEditor(onDropIntoEditor(editor))
×
116

117
      // Action commands
NEW
118
      console.log({ ack })
×
NEW
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

200
      <CollaborativeEditorWebSocketStatus
×
201
        className={styles.inlineStatus}
×
202
        status={websocketStatus}
×
203
      />
×
204

205
      {mode === 'preview' && (
×
206
        <section
×
207
          className={styles.previewPage}
×
208
          dangerouslySetInnerHTML={{ __html }}
×
209
        />
×
210
      )}
211

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

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