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

EcrituresNumeriques / stylo / 14472767041

15 Apr 2025 03:02PM UTC coverage: 33.491% (+2.1%) from 31.374%
14472767041

push

github

ggrossetie
fix: surcharge le style de liens externes pour ne pas afficher l'icone par défaut

515 of 777 branches covered (66.28%)

Branch coverage included in aggregate %.

5016 of 15738 relevant lines covered (31.87%)

2.3 hits per line

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

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

7
const sentryReduxEnhancer = Sentry.createReduxEnhancer()
1✔
8

9
const sessionTokenName = 'sessionToken'
1✔
10

11
function createReducer(initialState, handlers) {
25✔
12
  return function reducer(state = initialState, action) {
25✔
13
    if (Object.prototype.hasOwnProperty.call(handlers, action.type)) {
26✔
14
      return handlers[action.type](state, action)
1✔
15
    } else {
26✔
16
      return state
25✔
17
    }
25✔
18
  }
26✔
19
}
25✔
20

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

90
function createRootReducer(state) {
25✔
91
  return createReducer(state, {
25✔
92
    PROFILE: setProfile,
25✔
93
    SET_AUTH_TOKEN: setAuthToken,
25✔
94
    LOGIN: loginUser,
25✔
95
    UPDATE_SESSION_TOKEN: setSessionToken,
25✔
96
    UPDATE_ACTIVE_USER_DETAILS: updateActiveUserDetails,
25✔
97
    LOGOUT: logoutUser,
25✔
98

99
    // article reducers
25✔
100
    UPDATE_ARTICLE_STATS: updateArticleStats,
25✔
101
    UPDATE_ARTICLE_STRUCTURE: updateArticleStructure,
25✔
102
    UPDATE_ARTICLE_WRITERS: updateArticleWriters,
25✔
103
    UPDATE_ARTICLE_WORKING_COPY_STATUS: updateArticleWorkingCopyStatus,
25✔
104

105
    // user preferences reducers
25✔
106
    USER_PREFERENCES_TOGGLE: toggleUserPreferences,
25✔
107
    SET_EXPORT_PREFERENCES: setExportPreferences,
25✔
108

109
    SET_ARTICLE_VERSIONS: setArticleVersions,
25✔
110
    SET_WORKING_ARTICLE_UPDATED_AT: setWorkingArticleUpdatedAt,
25✔
111
    SET_WORKING_ARTICLE_TEXT: setWorkingArticleText,
25✔
112
    SET_WORKING_ARTICLE_METADATA: setWorkingArticleMetadata,
25✔
113
    SET_WORKING_ARTICLE_BIBLIOGRAPHY: setWorkingArticleBibliography,
25✔
114
    SET_WORKING_ARTICLE_STATE: setWorkingArticleState,
25✔
115
    SET_CREATE_ARTICLE_VERSION_ERROR: setCreateArticleVersionError,
25✔
116

117
    ARTICLE_PREFERENCES_TOGGLE: toggleArticlePreferences,
25✔
118

119
    UPDATE_EDITOR_CURSOR_POSITION: updateEditorCursorPosition,
25✔
120

121
    SET_ACTIVE_WORKSPACE: setActiveWorkspace,
25✔
122

123
    UPDATE_SELECTED_TAG: updateSelectedTag,
25✔
124
  })
25✔
125
}
25✔
126

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

243
function persistStateIntoLocalStorage({ getState }) {
25✔
244
  const actionStateMap = new Map([
25✔
245
    ['ARTICLE_PREFERENCES_TOGGLE', 'articlePreferences'],
25✔
246
    ['USER_PREFERENCES_TOGGLE', 'userPreferences'],
25✔
247
    ['SET_EXPORT_PREFERENCES', 'exportPreferences'],
25✔
248
  ])
25✔
249

250
  return (next) => {
25✔
251
    return (action) => {
25✔
252
      if (actionStateMap.has(action.type)) {
1!
253
        const key = actionStateMap.get(action.type)
×
254
        // we run the reducer first
×
255
        next(action)
×
256
        // we fetch the updated state
×
257
        const state = getState()[key]
×
258

259
        // we persist it for a later page reload
×
260
        localStorage.setItem(key, JSON.stringify(state))
×
261

262
        return
×
263
      } else if (action.type === 'LOGOUT') {
1!
264
        const { backendEndpoint } = applicationConfig
×
265
        localStorage.removeItem('articlePreferences')
×
266
        localStorage.removeItem('userPreferences')
×
267
        document.location.replace(backendEndpoint + '/logout')
×
268
      }
×
269

270
      if (action.type === 'LOGIN' || action.type === 'UPDATE_SESSION_TOKEN') {
1!
271
        next(action)
1✔
272
        const { sessionToken } = getState()
1✔
273
        localStorage.setItem(sessionTokenName, sessionToken)
1✔
274
        return
1✔
275
      }
1!
276

277
      if (action.type === 'LOGOUT') {
×
278
        localStorage.removeItem(sessionTokenName)
×
279
        return next(action)
×
280
      }
×
281

282
      return next(action)
×
283
    }
1✔
284
  }
25✔
285
}
25✔
286

287
function setProfile(state, action) {
×
288
  const { user } = action
×
289
  if (!user) {
×
290
    return { ...state, activeUser: undefined, hasBooted: true }
×
291
  }
×
292
  return {
×
293
    ...state,
×
294
    hasBooted: true,
×
295
    loggedIn: true,
×
296
    activeUser: {
×
297
      ...state.activeUser,
×
298
      activeWorkspaceId: action.activeWorkspaceId,
×
299
      ...user,
×
300
    },
×
301
  }
×
302
}
×
303

304
function setAuthToken(state, { service, token = null }) {
×
305
  if (service === 'zotero') {
×
306
    return {
×
307
      ...state,
×
308
      activeUser: {
×
309
        ...state.activeUser,
×
310
        zoteroToken: token,
×
311
      },
×
312
    }
×
313
  }
×
314
}
×
315

316
function setSessionToken(state, { token: sessionToken }) {
×
317
  return {
×
318
    ...state,
×
319
    sessionToken,
×
320
  }
×
321
}
×
322

323
function loginUser(state, { user, token: sessionToken }) {
1✔
324
  if (sessionToken) {
1!
325
    Sentry.setUser({ id: user._id })
×
326
    return {
×
327
      ...state,
×
328
      sessionToken,
×
329
      activeUser: {
×
330
        ...state.user,
×
331
        ...user,
×
332
        // dates are expected to be in timestamp string format (including milliseconds)
×
333
        createdAt: String(new Date(user.createdAt).getTime()),
×
334
        updatedAt: String(new Date(user.updatedAt).getTime()),
×
335
      },
×
336
    }
×
337
  }
×
338

339
  return state
1✔
340
}
1✔
341

342
function updateActiveUserDetails(state, action) {
×
343
  return {
×
344
    ...state,
×
345
    activeUser: { ...state.activeUser, ...action.payload },
×
346
  }
×
347
}
×
348

349
function logoutUser(state) {
×
350
  Sentry.setUser(null)
×
351
  return { ...state, ...initialState }
×
352
}
×
353

354
const SPACE_RE = /\s+/gi
1✔
355
const CITATION_RE = /(\[@[\w-]+)/gi
1✔
356
const REMOVE_MARKDOWN_RE = /[#_*]+\s?/gi
1✔
357

358
function updateArticleStats(state, { md }) {
×
359
  const text = (md || '').trim()
×
360

361
  const textWithoutMarkdown = text.replace(REMOVE_MARKDOWN_RE, '')
×
362
  const wordCount = textWithoutMarkdown.replace(SPACE_RE, ' ').split(' ').length
×
363

364
  const charCountNoSpace = textWithoutMarkdown.replace(SPACE_RE, '').length
×
365
  const charCountPlusSpace = textWithoutMarkdown.length
×
366
  const citationNb = text.match(CITATION_RE)?.length || 0
×
367

368
  return {
×
369
    ...state,
×
370
    articleStats: {
×
371
      wordCount,
×
372
      charCountNoSpace,
×
373
      charCountPlusSpace,
×
374
      citationNb,
×
375
    },
×
376
  }
×
377
}
×
378

379
function updateArticleStructure(state, { md }) {
×
380
  const text = (md || '').trim()
×
381
  const articleStructure = text
×
382
    .split('\n')
×
383
    .map((line, index) => ({ line, index }))
×
384
    .filter((lineWithIndex) => lineWithIndex.line.match(/^##+ /))
×
385
    .map((lineWithIndex) => {
×
386
      const title = lineWithIndex.line
×
387
        .replace(/##/, '')
×
388
        //arrow backspace (\u21B3)
×
389
        .replace(/#\s/g, '\u21B3')
×
390
        // middle dot (\u00B7) + non-breaking space (\xa0)
×
391
        .replace(/#/g, '\u00B7\xa0')
×
392
      return { ...lineWithIndex, title }
×
393
    })
×
394

395
  return { ...state, articleStructure }
×
396
}
×
397

398
function updateArticleWriters(state, { articleWriters }) {
×
399
  return { ...state, articleWriters }
×
400
}
×
401

402
function updateArticleWorkingCopyStatus(state, { status }) {
×
403
  return {
×
404
    ...state,
×
405
    articleWorkingCopy: { ...state.articleWorkingCopy, status },
×
406
  }
×
407
}
×
408

409
function setArticleVersions(state, { versions }) {
×
410
  return { ...state, articleVersions: versions }
×
411
}
×
412

413
function setCreateArticleVersionError(state, { err }) {
×
414
  return { ...state, createArticleVersionError: err }
×
415
}
×
416

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

422
function setWorkingArticleText(state, { text }) {
×
423
  const { workingArticle } = state
×
424
  return { ...state, workingArticle: { ...workingArticle, text } }
×
425
}
×
426

427
function setWorkingArticleMetadata(state, { metadata }) {
×
428
  const { workingArticle } = state
×
429
  return { ...state, workingArticle: { ...workingArticle, metadata } }
×
430
}
×
431

432
function setWorkingArticleBibliography(state, { bibliography }) {
×
433
  const bibTeXEntries = toEntries(bibliography)
×
434
  const { workingArticle } = state
×
435
  return {
×
436
    ...state,
×
437
    workingArticle: {
×
438
      ...workingArticle,
×
439
      bibliography: { text: bibliography, entries: bibTeXEntries },
×
440
    },
×
441
  }
×
442
}
×
443

444
function setWorkingArticleState(state, { workingArticleState, message }) {
×
445
  const { workingArticle } = state
×
446
  return {
×
447
    ...state,
×
448
    workingArticle: {
×
449
      ...workingArticle,
×
450
      state: workingArticleState,
×
451
      stateMessage: message,
×
452
    },
×
453
  }
×
454
}
×
455

456
function togglePreferences(storeKey) {
34✔
457
  return function togglePreferencesReducer(state, { key, value }) {
34✔
458
    const preferences = state[storeKey]
×
459

460
    return {
×
461
      ...state,
×
462
      [storeKey]: {
×
463
        ...preferences,
×
464
        [key]: value === undefined ? !preferences[key] : value,
×
465
      },
×
466
    }
×
467
  }
×
468
}
34✔
469

470
function setPreferences(storeKey) {
17✔
471
  return function setPreferencesReducer(state, { key, value }) {
17✔
472
    const preferences = state[storeKey]
×
473

474
    return {
×
475
      ...state,
×
476
      [storeKey]: {
×
477
        ...preferences,
×
478
        [key]: value,
×
479
      },
×
480
    }
×
481
  }
×
482
}
17✔
483

484
const toggleArticlePreferences = togglePreferences('articlePreferences')
1✔
485
const toggleUserPreferences = togglePreferences('userPreferences')
1✔
486
const setExportPreferences = setPreferences('exportPreferences')
1✔
487

488
function updateEditorCursorPosition(state, { lineNumber, column }) {
×
489
  return {
×
490
    ...state,
×
491
    editorCursorPosition: {
×
492
      lineNumber,
×
493
      column,
×
494
    },
×
495
  }
×
496
}
×
497

498
function setActiveWorkspace(state, { workspaceId }) {
×
499
  return {
×
500
    ...state,
×
501
    activeUser: {
×
502
      ...state.activeUser,
×
503
      activeWorkspaceId: workspaceId,
×
504
    },
×
505
  }
×
506
}
×
507

508
function updateSelectedTag(state, { tagId }) {
×
509
  const { selectedTagIds } = state.activeUser
×
510
  return {
×
511
    ...state,
×
512
    activeUser: {
×
513
      ...state.activeUser,
×
514
      selectedTagIds: selectedTagIds.includes(tagId)
×
515
        ? selectedTagIds.filter((selectedTagId) => selectedTagId !== tagId)
×
516
        : [...selectedTagIds, tagId],
×
517
    },
×
518
  }
×
519
}
×
520

521
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
1✔
522

523
export default function createReduxStore(state = initialState) {
1✔
524
  return createStore(
25✔
525
    createRootReducer(state),
25✔
526
    composeEnhancers(
25✔
527
      applyMiddleware(createNewArticleVersion, persistStateIntoLocalStorage),
25✔
528
      sentryReduxEnhancer
25✔
529
    )
25✔
530
  )
25✔
531
}
25✔
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