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

EcrituresNumeriques / stylo / 13548057613

26 Feb 2025 03:57PM UTC coverage: 12.824% (+0.8%) from 12.024%
13548057613

push

github

web-flow
Merge pull request #1272 from EcrituresNumeriques/fix/739

170 of 317 branches covered (53.63%)

Branch coverage included in aggregate %.

74 of 172 new or added lines in 10 files covered. (43.02%)

1 existing line in 1 file now uncovered.

1235 of 10639 relevant lines covered (11.61%)

1.86 hits per line

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

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

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

9
const sessionTokenName = 'sessionToken'
1✔
10

11
function createReducer(initialState, handlers) {
10✔
12
  return function reducer(state = initialState, action) {
10✔
13
    if (Object.prototype.hasOwnProperty.call(handlers, action.type)) {
10!
14
      return handlers[action.type](state, action)
×
15
    } else {
10✔
16
      return state
10✔
17
    }
10✔
18
  }
10✔
19
}
10✔
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
  articleStructure: [],
1✔
33
  articleVersions: [],
1✔
34
  createArticleVersionError: null,
1✔
35
  articleWriters: [],
1✔
36
  articlePreferences: localStorage.getItem('articlePreferences')
1✔
37
    ? JSON.parse(localStorage.getItem('articlePreferences'))
1!
38
    : {
1✔
39
        expandSidebarLeft: true,
1✔
40
        expandSidebarRight: false,
1✔
41
        metadataFormMode: 'basic',
1✔
42
        expandVersions: false,
1✔
43
      },
1✔
44
  articleFilters: {
1✔
45
    tagIds: [],
1✔
46
    text: '',
1✔
47
  },
1✔
48
  articleStats: {
1✔
49
    wordCount: 0,
1✔
50
    charCountNoSpace: 0,
1✔
51
    charCountPlusSpace: 0,
1✔
52
    citationNb: 0,
1✔
53
  },
1✔
54
  // Active user (authenticated)
1✔
55
  activeUser: {
1✔
56
    authType: null,
1✔
57
    authTypes: [],
1✔
58
    zoteroToken: null,
1✔
59
    selectedTagIds: [],
1✔
60
    workspaces: [],
1✔
61
    activeWorkspaceId: null,
1✔
62
  },
1✔
63
  latestTagCreated: null,
1✔
64
  latestCorpusCreated: null,
1✔
65
  latestCorpusDeleted: null,
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) {
10✔
91
  return createReducer(state, {
10✔
92
    PROFILE: setProfile,
10✔
93
    SET_AUTH_TOKEN: setAuthToken,
10✔
94
    LOGIN: loginUser,
10✔
95
    UPDATE_SESSION_TOKEN: setSessionToken,
10✔
96
    UPDATE_ACTIVE_USER_DETAILS: updateActiveUserDetails,
10✔
97
    LOGOUT: logoutUser,
10✔
98

99
    // article reducers
10✔
100
    UPDATE_ARTICLE_STATS: updateArticleStats,
10✔
101
    UPDATE_ARTICLE_STRUCTURE: updateArticleStructure,
10✔
102
    UPDATE_ARTICLE_WRITERS: updateArticleWriters,
10✔
103

104
    // user preferences reducers
10✔
105
    USER_PREFERENCES_TOGGLE: toggleUserPreferences,
10✔
106
    SET_EXPORT_PREFERENCES: setExportPreferences,
10✔
107

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

116
    ARTICLE_PREFERENCES_TOGGLE: toggleArticlePreferences,
10✔
117

118
    UPDATE_EDITOR_CURSOR_POSITION: updateEditorCursorPosition,
10✔
119

120
    SET_ACTIVE_WORKSPACE: setActiveWorkspace,
10✔
121

122
    UPDATE_SELECTED_TAG: updateSelectedTag,
10✔
123
    TAG_CREATED: tagCreated,
10✔
124

125
    SET_LATEST_CORPUS_DELETED: setLatestCorpusDeleted,
10✔
126
    SET_LATEST_CORPUS_CREATED: setLatestCorpusCreated,
10✔
127
    SET_LATEST_CORPUS_UPDATED: setLatestCorpusUpdated,
10✔
128
  })
10✔
129
}
10✔
130

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

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

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

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

266
        return
×
267
      } else if (action.type === 'LOGOUT') {
×
268
        const { backendEndpoint } = applicationConfig
×
269
        localStorage.removeItem('articlePreferences')
×
270
        localStorage.removeItem('userPreferences')
×
271
        document.location.replace(backendEndpoint + '/logout')
×
272
      }
×
273

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

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

286
      return next(action)
×
287
    }
×
288
  }
10✔
289
}
10✔
290

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

NEW
308
function setAuthToken(state, { service, token = null }) {
×
NEW
309
  if (service === 'zotero') {
×
NEW
310
    return {
×
NEW
311
      ...state,
×
NEW
312
      activeUser: {
×
NEW
313
        ...state.activeUser,
×
NEW
314
        zoteroToken: token,
×
NEW
315
      },
×
NEW
316
    }
×
317
  }
×
318
}
×
319

320
function setSessionToken(state, { token: sessionToken }) {
×
321
  return {
×
322
    ...state,
×
323
    sessionToken,
×
324
  }
×
325
}
×
326

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

343
  return state
×
344
}
×
345

346
function updateActiveUserDetails(state, action) {
×
347
  return {
×
348
    ...state,
×
349
    activeUser: { ...state.activeUser, ...action.payload },
×
350
  }
×
351
}
×
352

353
function logoutUser(state) {
×
354
  Sentry.setUser(null)
×
355
  return { ...state, ...initialState }
×
356
}
×
357

358
const SPACE_RE = /\s+/gi
1✔
359
const CITATION_RE = /(\[@[\w-]+)/gi
1✔
360
const REMOVE_MARKDOWN_RE = /[#_*]+\s?/gi
1✔
361

362
function updateArticleStats(state, { md }) {
×
363
  const text = (md || '').trim()
×
364

365
  const textWithoutMarkdown = text.replace(REMOVE_MARKDOWN_RE, '')
×
366
  const wordCount = textWithoutMarkdown.replace(SPACE_RE, ' ').split(' ').length
×
367

368
  const charCountNoSpace = textWithoutMarkdown.replace(SPACE_RE, '').length
×
369
  const charCountPlusSpace = textWithoutMarkdown.length
×
370
  const citationNb = text.match(CITATION_RE)?.length || 0
×
371

372
  return {
×
373
    ...state,
×
374
    articleStats: {
×
375
      wordCount,
×
376
      charCountNoSpace,
×
377
      charCountPlusSpace,
×
378
      citationNb,
×
379
    },
×
380
  }
×
381
}
×
382

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

399
  return { ...state, articleStructure }
×
400
}
×
401

402
function updateArticleWriters(state, { articleWriters }) {
×
403
  return { ...state, articleWriters }
×
404
}
×
405

406
function setArticleVersions(state, { versions }) {
×
407
  return { ...state, articleVersions: versions }
×
408
}
×
409

410
function setCreateArticleVersionError(state, { err }) {
×
411
  return { ...state, createArticleVersionError: err }
×
412
}
×
413

414
function setWorkingArticleUpdatedAt(state, { updatedAt }) {
×
415
  const { workingArticle } = state
×
416
  return { ...state, workingArticle: { ...workingArticle, updatedAt } }
×
417
}
×
418

419
function setWorkingArticleText(state, { text }) {
×
420
  const { workingArticle } = state
×
421
  return { ...state, workingArticle: { ...workingArticle, text } }
×
422
}
×
423

424
function setWorkingArticleMetadata(state, { metadata }) {
×
425
  const { workingArticle } = state
×
426
  return { ...state, workingArticle: { ...workingArticle, metadata } }
×
427
}
×
428

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

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

453
function togglePreferences(storeKey) {
26✔
454
  return function togglePreferencesReducer(state, { key, value }) {
26✔
455
    const preferences = state[storeKey]
×
456

457
    return {
×
458
      ...state,
×
459
      [storeKey]: {
×
460
        ...preferences,
×
461
        [key]: value === undefined ? !preferences[key] : value,
×
462
      },
×
463
    }
×
464
  }
×
465
}
26✔
466

467
function setPreferences(storeKey) {
13✔
468
  return function setPreferencesReducer(state, { key, value }) {
13✔
469
    const preferences = state[storeKey]
×
470

471
    return {
×
472
      ...state,
×
473
      [storeKey]: {
×
474
        ...preferences,
×
475
        [key]: value,
×
476
      },
×
477
    }
×
478
  }
×
479
}
13✔
480

481
const toggleArticlePreferences = togglePreferences('articlePreferences')
1✔
482
const toggleUserPreferences = togglePreferences('userPreferences')
1✔
483
const setExportPreferences = setPreferences('exportPreferences')
1✔
484

485
function updateEditorCursorPosition(state, { lineNumber, column }) {
×
486
  return {
×
487
    ...state,
×
488
    editorCursorPosition: {
×
489
      lineNumber,
×
490
      column,
×
491
    },
×
492
  }
×
493
}
×
494

495
function setActiveWorkspace(state, { workspaceId }) {
×
496
  return {
×
497
    ...state,
×
498
    activeUser: {
×
499
      ...state.activeUser,
×
500
      activeWorkspaceId: workspaceId,
×
501
    },
×
502
  }
×
503
}
×
504

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

518
function tagCreated(state, { tag }) {
×
519
  return {
×
520
    ...state,
×
521
    latestTagCreated: tag,
×
522
  }
×
523
}
×
524

525
function setLatestCorpusDeleted(state, { data }) {
×
526
  return {
×
527
    ...state,
×
528
    latestCorpusDeleted: data,
×
529
  }
×
530
}
×
531

532
function setLatestCorpusCreated(state, { data }) {
×
533
  return {
×
534
    ...state,
×
535
    latestCorpusCreated: data,
×
536
  }
×
537
}
×
538

539
function setLatestCorpusUpdated(state, { data }) {
×
540
  return {
×
541
    ...state,
×
542
    latestCorpusUpdated: data,
×
543
  }
×
544
}
×
545

546
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
1✔
547

548
export default function createReduxStore(state = initialState) {
1✔
549
  return createStore(
10✔
550
    createRootReducer(state),
10✔
551
    composeEnhancers(
10✔
552
      applyMiddleware(createNewArticleVersion, persistStateIntoLocalStorage),
10✔
553
      sentryReduxEnhancer
10✔
554
    )
10✔
555
  )
10✔
556
}
10✔
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