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

EcrituresNumeriques / stylo / 14530927846

18 Apr 2025 06:33AM UTC coverage: 34.65% (+1.2%) from 33.412%
14530927846

push

github

web-flow
Ajout des providers Zotero et Hypothesis (#1376)

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

535 of 793 branches covered (67.47%)

Branch coverage included in aggregate %.

544 of 1053 new or added lines in 27 files covered. (51.66%)

51 existing lines in 9 files now uncovered.

5316 of 16093 relevant lines covered (33.03%)

2.4 hits per line

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

40.83
/front/src/createReduxStore.js
1
import * as Sentry from '@sentry/react'
1✔
2
import { applyMiddleware, compose, createStore } from 'redux'
1✔
3
import { toEntries } from './helpers/bibtex'
1✔
4
import ArticleService from './services/ArticleService'
1✔
5

6
const sentryReduxEnhancer = Sentry.createReduxEnhancer()
1✔
7
const sessionTokenName = 'sessionToken'
1✔
8

9
// Définition du store Redux et de l'ensemble des actions
1✔
10
export const initialState = {
1✔
11
  sessionToken: localStorage.getItem(sessionTokenName),
1✔
12
  workingArticle: {
1✔
13
    state: 'saved',
1✔
14
    bibliography: {
1✔
15
      text: '',
1✔
16
      entries: [],
1✔
17
    },
1✔
18
  },
1✔
19
  articleWorkingCopy: {
1✔
20
    status: 'synced',
1✔
21
  },
1✔
22
  articleStructure: [],
1✔
23
  articleVersions: [],
1✔
24
  createArticleVersionError: null,
1✔
25
  articleWriters: [],
1✔
26
  articlePreferences: localStorage.getItem('articlePreferences')
1✔
27
    ? JSON.parse(localStorage.getItem('articlePreferences'))
1!
28
    : {
1✔
29
        expandSidebarLeft: true,
1✔
30
        expandSidebarRight: false,
1✔
31
        metadataFormMode: 'basic',
1✔
32
        expandVersions: false,
1✔
33
      },
1✔
34
  articleFilters: {
1✔
35
    tagIds: [],
1✔
36
    text: '',
1✔
37
  },
1✔
38
  articleStats: {
1✔
39
    wordCount: 0,
1✔
40
    charCountNoSpace: 0,
1✔
41
    charCountPlusSpace: 0,
1✔
42
    citationNb: 0,
1✔
43
  },
1✔
44
  // Active user (authenticated)
1✔
45
  activeUser: {
1✔
46
    authTypes: [],
1✔
47
    authProviders: {},
1✔
48
    selectedTagIds: [],
1✔
49
    workspaces: [],
1✔
50
    activeWorkspaceId: null,
1✔
51
  },
1✔
52
  userPreferences: localStorage.getItem('userPreferences')
1✔
53
    ? JSON.parse(localStorage.getItem('userPreferences'))
1!
54
    : {
1✔
55
        // The user we impersonate
1✔
56
        currentUser: null,
1✔
57
        trackingConsent: true /* default value should be false */,
1✔
58
      },
1✔
59
  exportPreferences: localStorage.getItem('exportPreferences')
1✔
60
    ? JSON.parse(localStorage.getItem('exportPreferences'))
1!
61
    : {
1✔
62
        bibliography_style: 'chicagomodified',
1✔
63
        with_toc: 0,
1✔
64
        link_citations: 0,
1✔
65
        with_nocite: 0,
1✔
66
        formats: 'html',
1✔
67
        unnumbered: 0,
1✔
68
        book_division: 'part',
1✔
69
      },
1✔
70
  editorCursorPosition: {
1✔
71
    lineNumber: 0,
1✔
72
    column: 0,
1✔
73
  },
1✔
74
}
1✔
75

76
/**
1✔
77
 *
1✔
78
 * @param {*} state
1✔
79
 * @returns
1✔
80
 */
1✔
81
function createReducer(initialState, handlers) {
28✔
82
  return function reducer(state = initialState, action) {
28✔
83
    if (Object.prototype.hasOwnProperty.call(handlers, action.type)) {
31✔
84
      return handlers[action.type](state, action)
3✔
85
    } else {
31✔
86
      return state
28✔
87
    }
28✔
88
  }
31✔
89
}
28✔
90

91
/**
1✔
92
 *
1✔
93
 * @param {*} state
1✔
94
 * @returns
1✔
95
 */
1✔
96
function createRootReducer(state) {
28✔
97
  return createReducer(state, {
28✔
98
    PROFILE: setProfile,
28✔
99
    LOGIN: loginUser,
28✔
100
    UPDATE_SESSION_TOKEN: setSessionToken,
28✔
101
    UPDATE_ACTIVE_USER_DETAILS: updateActiveUserDetails,
28✔
102
    LOGOUT: logoutUser,
28✔
103

104
    // article reducers
28✔
105
    UPDATE_ARTICLE_STATS: updateArticleStats,
28✔
106
    UPDATE_ARTICLE_STRUCTURE: updateArticleStructure,
28✔
107
    UPDATE_ARTICLE_WRITERS: updateArticleWriters,
28✔
108
    UPDATE_ARTICLE_WORKING_COPY_STATUS: updateArticleWorkingCopyStatus,
28✔
109

110
    // user preferences reducers
28✔
111
    USER_PREFERENCES_TOGGLE: toggleUserPreferences,
28✔
112
    SET_EXPORT_PREFERENCES: setExportPreferences,
28✔
113

114
    SET_ARTICLE_VERSIONS: setArticleVersions,
28✔
115
    SET_WORKING_ARTICLE_UPDATED_AT: setWorkingArticleUpdatedAt,
28✔
116
    SET_WORKING_ARTICLE_TEXT: setWorkingArticleText,
28✔
117
    SET_WORKING_ARTICLE_METADATA: setWorkingArticleMetadata,
28✔
118
    SET_WORKING_ARTICLE_BIBLIOGRAPHY: setWorkingArticleBibliography,
28✔
119
    SET_WORKING_ARTICLE_STATE: setWorkingArticleState,
28✔
120
    SET_CREATE_ARTICLE_VERSION_ERROR: setCreateArticleVersionError,
28✔
121

122
    ARTICLE_PREFERENCES_TOGGLE: toggleArticlePreferences,
28✔
123

124
    UPDATE_EDITOR_CURSOR_POSITION: updateEditorCursorPosition,
28✔
125

126
    SET_ACTIVE_WORKSPACE: setActiveWorkspace,
28✔
127

128
    UPDATE_SELECTED_TAG: updateSelectedTag,
28✔
129
  })
28✔
130
}
28✔
131

132
const createNewArticleVersion = (store) => {
1✔
133
  return (next) => {
28✔
134
    return async (action) => {
28✔
135
      if (action.type === 'CREATE_NEW_ARTICLE_VERSION') {
3!
136
        const { activeUser, sessionToken, userPreferences } = store.getState()
×
137
        const userId = userPreferences.currentUser ?? activeUser._id
×
138
        const { articleId, major, message } = action
×
139
        const articleService = new ArticleService(
×
140
          userId,
×
141
          articleId,
×
142
          sessionToken
×
143
        )
×
144
        try {
×
145
          const response = await articleService.createNewVersion(major, message)
×
146
          store.dispatch({
×
147
            type: 'SET_ARTICLE_VERSIONS',
×
148
            versions: response.article.createVersion.versions,
×
149
          })
×
150
        } catch (err) {
×
151
          store.dispatch({ type: 'SET_CREATE_ARTICLE_VERSION_ERROR', err: err })
×
152
        }
×
153
        return next(action)
×
154
      }
×
155
      if (action.type === 'UPDATE_WORKING_ARTICLE_TEXT') {
3!
156
        const { activeUser, sessionToken, userPreferences } = store.getState()
×
157
        const userId = userPreferences.currentUser ?? activeUser._id
×
158
        const { articleId, text } = action
×
159
        try {
×
160
          const { article } = await new ArticleService(
×
161
            userId,
×
162
            articleId,
×
163
            sessionToken
×
164
          ).saveText(text)
×
165
          store.dispatch({
×
166
            type: 'SET_WORKING_ARTICLE_STATE',
×
167
            workingArticleState: 'saved',
×
168
          })
×
169
          store.dispatch({ type: 'SET_WORKING_ARTICLE_TEXT', text })
×
170
          store.dispatch({
×
171
            type: 'SET_WORKING_ARTICLE_UPDATED_AT',
×
172
            updatedAt: article.updateWorkingVersion.updatedAt,
×
173
          })
×
174
        } catch (err) {
×
175
          console.error(err)
×
176
          store.dispatch({
×
177
            type: 'SET_WORKING_ARTICLE_STATE',
×
178
            workingArticleState: 'saveFailure',
×
179
            message: err.message,
×
180
          })
×
181
        }
×
182
        return next(action)
×
183
      }
×
184
      if (action.type === 'UPDATE_WORKING_ARTICLE_METADATA') {
3!
185
        const { activeUser, sessionToken, userPreferences } = store.getState()
×
186
        const userId = userPreferences.currentUser ?? activeUser._id
×
187
        const { articleId, metadata } = action
×
188
        try {
×
189
          const { article } = await new ArticleService(
×
190
            userId,
×
191
            articleId,
×
192
            sessionToken
×
193
          ).saveMetadata(metadata)
×
194
          store.dispatch({
×
195
            type: 'SET_WORKING_ARTICLE_STATE',
×
196
            workingArticleState: 'saved',
×
197
          })
×
198
          store.dispatch({ type: 'SET_WORKING_ARTICLE_METADATA', metadata })
×
199
          store.dispatch({
×
200
            type: 'SET_WORKING_ARTICLE_UPDATED_AT',
×
201
            updatedAt: article.updateWorkingVersion.updatedAt,
×
202
          })
×
203
        } catch (err) {
×
204
          console.error(err)
×
205
          store.dispatch({
×
206
            type: 'SET_WORKING_ARTICLE_STATE',
×
207
            workingArticleState: 'saveFailure',
×
208
          })
×
209
        }
×
210
        return next(action)
×
211
      }
×
212
      if (action.type === 'UPDATE_WORKING_ARTICLE_BIBLIOGRAPHY') {
3!
213
        const { activeUser, sessionToken, userPreferences } = store.getState()
×
214
        const userId = userPreferences.currentUser ?? activeUser._id
×
215
        const { articleId, bibliography } = action
×
216
        try {
×
217
          const { article } = await new ArticleService(
×
218
            userId,
×
219
            articleId,
×
220
            sessionToken
×
221
          ).saveBibliography(bibliography)
×
222
          store.dispatch({
×
223
            type: 'SET_WORKING_ARTICLE_STATE',
×
224
            workingArticleState: 'saved',
×
225
          })
×
226
          store.dispatch({
×
227
            type: 'SET_WORKING_ARTICLE_BIBLIOGRAPHY',
×
228
            bibliography,
×
229
          })
×
230
          store.dispatch({
×
231
            type: 'SET_WORKING_ARTICLE_UPDATED_AT',
×
232
            updatedAt: article.updateWorkingVersion.updatedAt,
×
233
          })
×
234
        } catch (err) {
×
235
          console.error(err)
×
236
          store.dispatch({
×
237
            type: 'SET_WORKING_ARTICLE_STATE',
×
238
            workingArticleState: 'saveFailure',
×
239
          })
×
240
        }
×
241
        return next(action)
×
242
      }
×
243
      return next(action)
3✔
244
    }
3✔
245
  }
28✔
246
}
28✔
247

248
function persistStateIntoLocalStorage({ getState }) {
28✔
249
  const actionStateMap = new Map([
28✔
250
    ['ARTICLE_PREFERENCES_TOGGLE', 'articlePreferences'],
28✔
251
    ['USER_PREFERENCES_TOGGLE', 'userPreferences'],
28✔
252
    ['SET_EXPORT_PREFERENCES', 'exportPreferences'],
28✔
253
  ])
28✔
254

255
  return (next) => {
28✔
256
    return (action) => {
28✔
257
      if (actionStateMap.has(action.type)) {
3!
258
        const key = actionStateMap.get(action.type)
×
259
        // we run the reducer first
×
260
        next(action)
×
261
        // we fetch the updated state
×
262
        const state = getState()[key]
×
263

264
        // we persist it for a later page reload
×
265
        localStorage.setItem(key, JSON.stringify(state))
×
266

267
        return
×
268
      } else if (action.type === 'LOGOUT') {
3!
269
        localStorage.removeItem('articlePreferences')
×
270
        localStorage.removeItem('userPreferences')
×
271
      }
×
272

273
      if (action.type === 'LOGIN' || action.type === 'UPDATE_SESSION_TOKEN') {
3✔
274
        next(action)
1✔
275
        const { sessionToken } = getState()
1✔
276
        localStorage.setItem(sessionTokenName, sessionToken)
1✔
277
        return
1✔
278
      }
1✔
279

280
      if (action.type === 'LOGOUT') {
2!
281
        localStorage.removeItem(sessionTokenName)
×
282
        return next(action)
×
283
      }
×
284

285
      return next(action)
2✔
286
    }
3✔
287
  }
28✔
288
}
28✔
289

290
function setProfile(state, action) {
×
291
  const { user } = action
×
292

293
  if (!user) {
×
NEW
294
    return { ...state, activeUser: undefined }
×
295
  }
×
296

297
  return {
×
298
    ...state,
×
299
    activeUser: {
×
300
      ...state.activeUser,
×
301
      activeWorkspaceId: action.activeWorkspaceId,
×
302
      ...user,
×
303
    },
×
304
  }
×
305
}
×
306

307
function setSessionToken(state, { token: sessionToken }) {
×
308
  return {
×
309
    ...state,
×
310
    sessionToken,
×
311
  }
×
312
}
×
313

314
function loginUser(state, { user, token: sessionToken }) {
1✔
315
  if (sessionToken) {
1!
316
    Sentry.setUser({ id: user._id })
×
317
    return {
×
318
      ...state,
×
319
      sessionToken,
×
320
      activeUser: {
×
321
        ...state.user,
×
322
        ...user,
×
323
        // dates are expected to be in timestamp string format (including milliseconds)
×
324
        createdAt: String(new Date(user.createdAt).getTime()),
×
325
        updatedAt: String(new Date(user.updatedAt).getTime()),
×
326
      },
×
327
    }
×
328
  }
×
329

330
  return state
1✔
331
}
1✔
332

333
function updateActiveUserDetails(state, action) {
2✔
334
  return {
2✔
335
    ...state,
2✔
336
    activeUser: { ...state.activeUser, ...action.payload },
2✔
337
  }
2✔
338
}
2✔
339

NEW
340
function logoutUser() {
×
NEW
341
  return structuredClone(initialState)
×
UNCOV
342
}
×
343

344
const SPACE_RE = /\s+/gi
1✔
345
const CITATION_RE = /(\[@[\w-]+)/gi
1✔
346
const REMOVE_MARKDOWN_RE = /[#_*]+\s?/gi
1✔
347

348
function updateArticleStats(state, { md }) {
×
349
  const text = (md || '').trim()
×
350

351
  const textWithoutMarkdown = text.replace(REMOVE_MARKDOWN_RE, '')
×
352
  const wordCount = textWithoutMarkdown.replace(SPACE_RE, ' ').split(' ').length
×
353

354
  const charCountNoSpace = textWithoutMarkdown.replace(SPACE_RE, '').length
×
355
  const charCountPlusSpace = textWithoutMarkdown.length
×
356
  const citationNb = text.match(CITATION_RE)?.length || 0
×
357

358
  return {
×
359
    ...state,
×
360
    articleStats: {
×
361
      wordCount,
×
362
      charCountNoSpace,
×
363
      charCountPlusSpace,
×
364
      citationNb,
×
365
    },
×
366
  }
×
367
}
×
368

369
function updateArticleStructure(state, { md }) {
×
370
  const text = (md || '').trim()
×
371
  const articleStructure = text
×
372
    .split('\n')
×
373
    .map((line, index) => ({ line, index }))
×
374
    .filter((lineWithIndex) => lineWithIndex.line.match(/^##+ /))
×
375
    .map((lineWithIndex) => {
×
376
      const title = lineWithIndex.line
×
377
        .replace(/##/, '')
×
378
        //arrow backspace (\u21B3)
×
379
        .replace(/#\s/g, '\u21B3')
×
380
        // middle dot (\u00B7) + non-breaking space (\xa0)
×
381
        .replace(/#/g, '\u00B7\xa0')
×
382
      return { ...lineWithIndex, title }
×
383
    })
×
384

385
  return { ...state, articleStructure }
×
386
}
×
387

388
function updateArticleWriters(state, { articleWriters }) {
×
389
  return { ...state, articleWriters }
×
390
}
×
391

392
function updateArticleWorkingCopyStatus(state, { status }) {
×
393
  return {
×
394
    ...state,
×
395
    articleWorkingCopy: { ...state.articleWorkingCopy, status },
×
396
  }
×
397
}
×
398

399
function setArticleVersions(state, { versions }) {
×
400
  return { ...state, articleVersions: versions }
×
401
}
×
402

403
function setCreateArticleVersionError(state, { err }) {
×
404
  return { ...state, createArticleVersionError: err }
×
405
}
×
406

407
function setWorkingArticleUpdatedAt(state, { updatedAt }) {
×
408
  const { workingArticle } = state
×
409
  return { ...state, workingArticle: { ...workingArticle, updatedAt } }
×
410
}
×
411

412
function setWorkingArticleText(state, { text }) {
×
413
  const { workingArticle } = state
×
414
  return { ...state, workingArticle: { ...workingArticle, text } }
×
415
}
×
416

417
function setWorkingArticleMetadata(state, { metadata }) {
×
418
  const { workingArticle } = state
×
419
  return { ...state, workingArticle: { ...workingArticle, metadata } }
×
420
}
×
421

422
function setWorkingArticleBibliography(state, { bibliography }) {
×
423
  const bibTeXEntries = toEntries(bibliography)
×
424
  const { workingArticle } = state
×
425
  return {
×
426
    ...state,
×
427
    workingArticle: {
×
428
      ...workingArticle,
×
429
      bibliography: { text: bibliography, entries: bibTeXEntries },
×
430
    },
×
431
  }
×
432
}
×
433

434
function setWorkingArticleState(state, { workingArticleState, message }) {
×
435
  const { workingArticle } = state
×
436
  return {
×
437
    ...state,
×
438
    workingArticle: {
×
439
      ...workingArticle,
×
440
      state: workingArticleState,
×
441
      stateMessage: message,
×
442
    },
×
443
  }
×
444
}
×
445

446
function togglePreferences(storeKey) {
40✔
447
  return function togglePreferencesReducer(state, { key, value }) {
40✔
448
    const preferences = state[storeKey]
×
449

450
    return {
×
451
      ...state,
×
452
      [storeKey]: {
×
453
        ...preferences,
×
454
        [key]: value === undefined ? !preferences[key] : value,
×
455
      },
×
456
    }
×
457
  }
×
458
}
40✔
459

460
function setPreferences(storeKey) {
20✔
461
  return function setPreferencesReducer(state, { key, value }) {
20✔
462
    const preferences = state[storeKey]
×
463

464
    return {
×
465
      ...state,
×
466
      [storeKey]: {
×
467
        ...preferences,
×
468
        [key]: value,
×
469
      },
×
470
    }
×
471
  }
×
472
}
20✔
473

474
const toggleArticlePreferences = togglePreferences('articlePreferences')
1✔
475
const toggleUserPreferences = togglePreferences('userPreferences')
1✔
476
const setExportPreferences = setPreferences('exportPreferences')
1✔
477

478
function updateEditorCursorPosition(state, { lineNumber, column }) {
×
479
  return {
×
480
    ...state,
×
481
    editorCursorPosition: {
×
482
      lineNumber,
×
483
      column,
×
484
    },
×
485
  }
×
486
}
×
487

488
function setActiveWorkspace(state, { workspaceId }) {
×
489
  return {
×
490
    ...state,
×
491
    activeUser: {
×
492
      ...state.activeUser,
×
493
      activeWorkspaceId: workspaceId,
×
494
    },
×
495
  }
×
496
}
×
497

498
function updateSelectedTag(state, { tagId }) {
×
499
  const { selectedTagIds } = state.activeUser
×
500
  return {
×
501
    ...state,
×
502
    activeUser: {
×
503
      ...state.activeUser,
×
504
      selectedTagIds: selectedTagIds.includes(tagId)
×
505
        ? selectedTagIds.filter((selectedTagId) => selectedTagId !== tagId)
×
506
        : [...selectedTagIds, tagId],
×
507
    },
×
508
  }
×
509
}
×
510

511
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
1✔
512
  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
1!
NEW
513
      trace: true,
×
NEW
514
      traceLimit: 25,
×
NEW
515
    })
×
516
  : compose
1✔
517

518
export default function createReduxStore(state = {}) {
1✔
519
  return createStore(
28✔
520
    createRootReducer({
28✔
521
      ...structuredClone(initialState),
28✔
522
      ...structuredClone(state),
28✔
523
    }),
28✔
524
    composeEnhancers(
28✔
525
      applyMiddleware(createNewArticleVersion, persistStateIntoLocalStorage),
28✔
526
      sentryReduxEnhancer
28✔
527
    )
28✔
528
  )
28✔
529
}
28✔
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