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

EcrituresNumeriques / stylo / 12925854411

23 Jan 2025 09:11AM UTC coverage: 25.831% (-4.7%) from 30.523%
12925854411

push

github

web-flow
Merge pull request #1192 from EcrituresNumeriques/feat/vite6

322 of 518 branches covered (62.16%)

Branch coverage included in aggregate %.

3448 of 14077 relevant lines covered (24.49%)

1.66 hits per line

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

30.09
/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 WorkspaceService from './services/WorkspaceService.js'
1✔
6
import { applicationConfig } from './config.js'
1✔
7

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

10
const sessionTokenName = 'sessionToken'
1✔
11

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

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

91
function createRootReducer(state) {
6✔
92
  return createReducer(state, {
6✔
93
    PROFILE: setProfile,
6✔
94
    CLEAR_ZOTERO_TOKEN: clearZoteroToken,
6✔
95
    LOGIN: loginUser,
6✔
96
    UPDATE_SESSION_TOKEN: setSessionToken,
6✔
97
    UPDATE_ACTIVE_USER_DETAILS: updateActiveUserDetails,
6✔
98
    LOGOUT: logoutUser,
6✔
99

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

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

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

117
    ARTICLE_PREFERENCES_TOGGLE: toggleArticlePreferences,
6✔
118

119
    UPDATE_EDITOR_CURSOR_POSITION: updateEditorCursorPosition,
6✔
120

121
    SET_WORKSPACES: setWorkspaces,
6✔
122
    SET_ACTIVE_WORKSPACE: setActiveWorkspace,
6✔
123

124
    UPDATE_SELECTED_TAG: updateSelectedTag,
6✔
125
    TAG_CREATED: tagCreated,
6✔
126

127
    SET_LATEST_CORPUS_DELETED: setLatestCorpusDeleted,
6✔
128
    SET_LATEST_CORPUS_CREATED: setLatestCorpusCreated,
6✔
129
    SET_LATEST_CORPUS_UPDATED: setLatestCorpusUpdated,
6✔
130
  })
6✔
131
}
6✔
132

133
const createNewArticleVersion = (store) => {
1✔
134
  return (next) => {
6✔
135
    return async (action) => {
6✔
136
      if (action.type === 'CREATE_WORKSPACE') {
×
137
        const { activeUser, sessionToken } = store.getState()
×
138
        const workspaces = activeUser.workspaces
×
139
        const workspaceService = new WorkspaceService(sessionToken)
×
140
        const response = await workspaceService.create(action.data)
×
141
        store.dispatch({
×
142
          type: 'SET_WORKSPACES',
×
143
          workspaces: [response.createWorkspace, ...workspaces],
×
144
        })
×
145
        return next(action)
×
146
      }
×
147
      if (action.type === 'LEAVE_WORKSPACE') {
×
148
        const { activeUser, sessionToken } = store.getState()
×
149
        const workspaces = activeUser.workspaces
×
150
        const workspaceService = new WorkspaceService(sessionToken)
×
151
        const workspaceId = action.data.workspaceId
×
152
        await workspaceService.leave(workspaceId)
×
153
        store.dispatch({
×
154
          type: 'SET_WORKSPACES',
×
155
          workspaces: workspaces.filter((w) => w._id !== workspaceId),
×
156
        })
×
157
        return next(action)
×
158
      }
×
159
      if (action.type === 'CREATE_NEW_ARTICLE_VERSION') {
×
160
        const { activeUser, sessionToken, userPreferences } = store.getState()
×
161
        const userId = userPreferences.currentUser ?? activeUser._id
×
162
        const { articleId, major, message } = action
×
163
        const articleService = new ArticleService(
×
164
          userId,
×
165
          articleId,
×
166
          sessionToken
×
167
        )
×
168
        try {
×
169
          const response = await articleService.createNewVersion(major, message)
×
170
          store.dispatch({
×
171
            type: 'SET_ARTICLE_VERSIONS',
×
172
            versions: response.article.createVersion.versions,
×
173
          })
×
174
        } catch (err) {
×
175
          store.dispatch({ type: 'SET_CREATE_ARTICLE_VERSION_ERROR', err: err })
×
176
        }
×
177
        return next(action)
×
178
      }
×
179
      if (action.type === 'UPDATE_WORKING_ARTICLE_TEXT') {
×
180
        const { activeUser, sessionToken, userPreferences } = store.getState()
×
181
        const userId = userPreferences.currentUser ?? activeUser._id
×
182
        const { articleId, text } = action
×
183
        try {
×
184
          const { article } = await new ArticleService(
×
185
            userId,
×
186
            articleId,
×
187
            sessionToken
×
188
          ).saveText(text)
×
189
          store.dispatch({
×
190
            type: 'SET_WORKING_ARTICLE_STATE',
×
191
            workingArticleState: 'saved',
×
192
          })
×
193
          store.dispatch({ type: 'SET_WORKING_ARTICLE_TEXT', text })
×
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
            message: err.message,
×
204
          })
×
205
        }
×
206
        return next(action)
×
207
      }
×
208
      if (action.type === 'UPDATE_WORKING_ARTICLE_METADATA') {
×
209
        const { activeUser, sessionToken, userPreferences } = store.getState()
×
210
        const userId = userPreferences.currentUser ?? activeUser._id
×
211
        const { articleId, metadata } = action
×
212
        try {
×
213
          const { article } = await new ArticleService(
×
214
            userId,
×
215
            articleId,
×
216
            sessionToken
×
217
          ).saveMetadata(metadata)
×
218
          store.dispatch({
×
219
            type: 'SET_WORKING_ARTICLE_STATE',
×
220
            workingArticleState: 'saved',
×
221
          })
×
222
          store.dispatch({ type: 'SET_WORKING_ARTICLE_METADATA', metadata })
×
223
          store.dispatch({
×
224
            type: 'SET_WORKING_ARTICLE_UPDATED_AT',
×
225
            updatedAt: article.updateWorkingVersion.updatedAt,
×
226
          })
×
227
        } catch (err) {
×
228
          console.error(err)
×
229
          store.dispatch({
×
230
            type: 'SET_WORKING_ARTICLE_STATE',
×
231
            workingArticleState: 'saveFailure',
×
232
          })
×
233
        }
×
234
        return next(action)
×
235
      }
×
236
      if (action.type === 'UPDATE_WORKING_ARTICLE_BIBLIOGRAPHY') {
×
237
        const { activeUser, sessionToken, userPreferences } = store.getState()
×
238
        const userId = userPreferences.currentUser ?? activeUser._id
×
239
        const { articleId, bibliography } = action
×
240
        try {
×
241
          const { article } = await new ArticleService(
×
242
            userId,
×
243
            articleId,
×
244
            sessionToken
×
245
          ).saveBibliography(bibliography)
×
246
          store.dispatch({
×
247
            type: 'SET_WORKING_ARTICLE_STATE',
×
248
            workingArticleState: 'saved',
×
249
          })
×
250
          store.dispatch({
×
251
            type: 'SET_WORKING_ARTICLE_BIBLIOGRAPHY',
×
252
            bibliography,
×
253
          })
×
254
          store.dispatch({
×
255
            type: 'SET_WORKING_ARTICLE_UPDATED_AT',
×
256
            updatedAt: article.updateWorkingVersion.updatedAt,
×
257
          })
×
258
        } catch (err) {
×
259
          console.error(err)
×
260
          store.dispatch({
×
261
            type: 'SET_WORKING_ARTICLE_STATE',
×
262
            workingArticleState: 'saveFailure',
×
263
          })
×
264
        }
×
265
        return next(action)
×
266
      }
×
267
      return next(action)
×
268
    }
×
269
  }
6✔
270
}
6✔
271

272
function persistStateIntoLocalStorage({ getState }) {
6✔
273
  const actionStateMap = new Map([
6✔
274
    ['ARTICLE_PREFERENCES_TOGGLE', 'articlePreferences'],
6✔
275
    ['USER_PREFERENCES_TOGGLE', 'userPreferences'],
6✔
276
    ['SET_EXPORT_PREFERENCES', 'exportPreferences'],
6✔
277
  ])
6✔
278

279
  return (next) => {
6✔
280
    return (action) => {
6✔
281
      if (actionStateMap.has(action.type)) {
×
282
        const key = actionStateMap.get(action.type)
×
283
        // we run the reducer first
×
284
        next(action)
×
285
        // we fetch the updated state
×
286
        const state = getState()[key]
×
287

288
        // we persist it for a later page reload
×
289
        localStorage.setItem(key, JSON.stringify(state))
×
290

291
        return
×
292
      } else if (action.type === 'LOGOUT') {
×
293
        const { backendEndpoint } = applicationConfig
×
294
        localStorage.removeItem('articlePreferences')
×
295
        localStorage.removeItem('userPreferences')
×
296
        document.location.replace(backendEndpoint + '/logout')
×
297
      }
×
298

299
      if (action.type === 'LOGIN' || action.type === 'UPDATE_SESSION_TOKEN') {
×
300
        next(action)
×
301
        const { sessionToken } = getState()
×
302
        localStorage.setItem(sessionTokenName, sessionToken)
×
303
        return
×
304
      }
×
305

306
      if (action.type === 'LOGOUT') {
×
307
        localStorage.removeItem(sessionTokenName)
×
308
        return next(action)
×
309
      }
×
310

311
      return next(action)
×
312
    }
×
313
  }
6✔
314
}
6✔
315

316
function setProfile(state, action) {
×
317
  const { user } = action
×
318
  if (!user) {
×
319
    return { ...state, activeUser: undefined, hasBooted: true }
×
320
  }
×
321
  return {
×
322
    ...state,
×
323
    hasBooted: true,
×
324
    loggedIn: true,
×
325
    activeUser: {
×
326
      ...state.activeUser,
×
327
      activeWorkspaceId: action.activeWorkspaceId,
×
328
      ...user,
×
329
    },
×
330
  }
×
331
}
×
332

333
function clearZoteroToken(state) {
×
334
  return {
×
335
    ...state,
×
336
    activeUser: {
×
337
      ...state.activeUser,
×
338
      zoteroToken: null,
×
339
    },
×
340
  }
×
341
}
×
342

343
function setSessionToken(state, { token: sessionToken }) {
×
344
  return {
×
345
    ...state,
×
346
    sessionToken,
×
347
  }
×
348
}
×
349

350
function loginUser(state, { user, token: sessionToken }) {
×
351
  if (sessionToken) {
×
352
    Sentry.setUser({ id: user._id })
×
353
    return {
×
354
      ...state,
×
355
      sessionToken,
×
356
      activeUser: {
×
357
        ...state.user,
×
358
        ...user,
×
359
        // dates are expected to be in timestamp string format (including milliseconds)
×
360
        createdAt: String(new Date(user.createdAt).getTime()),
×
361
        updatedAt: String(new Date(user.updatedAt).getTime()),
×
362
      },
×
363
    }
×
364
  }
×
365

366
  return state
×
367
}
×
368

369
function updateActiveUserDetails(state, action) {
×
370
  return {
×
371
    ...state,
×
372
    activeUser: { ...state.activeUser, ...action.payload },
×
373
  }
×
374
}
×
375

376
function logoutUser(state) {
×
377
  Sentry.setUser(null)
×
378
  return { ...state, ...initialState }
×
379
}
×
380

381
const SPACE_RE = /\s+/gi
1✔
382
const CITATION_RE = /(\[@[\w-]+)/gi
1✔
383
const REMOVE_MARKDOWN_RE = /[#_*]+\s?/gi
1✔
384

385
function updateArticleStats(state, { md }) {
×
386
  const text = (md || '').trim()
×
387

388
  const textWithoutMarkdown = text.replace(REMOVE_MARKDOWN_RE, '')
×
389
  const wordCount = textWithoutMarkdown.replace(SPACE_RE, ' ').split(' ').length
×
390

391
  const charCountNoSpace = textWithoutMarkdown.replace(SPACE_RE, '').length
×
392
  const charCountPlusSpace = textWithoutMarkdown.length
×
393
  const citationNb = text.match(CITATION_RE)?.length || 0
×
394

395
  return {
×
396
    ...state,
×
397
    articleStats: {
×
398
      wordCount,
×
399
      charCountNoSpace,
×
400
      charCountPlusSpace,
×
401
      citationNb,
×
402
    },
×
403
  }
×
404
}
×
405

406
function updateArticleStructure(state, { md }) {
×
407
  const text = (md || '').trim()
×
408
  const articleStructure = text
×
409
    .split('\n')
×
410
    .map((line, index) => ({ line, index }))
×
411
    .filter((lineWithIndex) => lineWithIndex.line.match(/^##+ /))
×
412
    .map((lineWithIndex) => {
×
413
      const title = lineWithIndex.line
×
414
        .replace(/##/, '')
×
415
        //arrow backspace (\u21B3)
×
416
        .replace(/#\s/g, '\u21B3')
×
417
        // middle dot (\u00B7) + non-breaking space (\xa0)
×
418
        .replace(/#/g, '\u00B7\xa0')
×
419
      return { ...lineWithIndex, title }
×
420
    })
×
421

422
  return { ...state, articleStructure }
×
423
}
×
424

425
function updateArticleWriters(state, { articleWriters }) {
×
426
  return { ...state, articleWriters }
×
427
}
×
428

429
function setArticleVersions(state, { versions }) {
×
430
  return { ...state, articleVersions: versions }
×
431
}
×
432

433
function setCreateArticleVersionError(state, { err }) {
×
434
  return { ...state, createArticleVersionError: err }
×
435
}
×
436

437
function setWorkingArticleUpdatedAt(state, { updatedAt }) {
×
438
  const { workingArticle } = state
×
439
  return { ...state, workingArticle: { ...workingArticle, updatedAt } }
×
440
}
×
441

442
function setWorkingArticleText(state, { text }) {
×
443
  const { workingArticle } = state
×
444
  return { ...state, workingArticle: { ...workingArticle, text } }
×
445
}
×
446

447
function setWorkingArticleMetadata(state, { metadata }) {
×
448
  const { workingArticle } = state
×
449
  return { ...state, workingArticle: { ...workingArticle, metadata } }
×
450
}
×
451

452
function setWorkingArticleBibliography(state, { bibliography }) {
×
453
  const bibTeXEntries = toEntries(bibliography)
×
454
  const { workingArticle } = state
×
455
  return {
×
456
    ...state,
×
457
    workingArticle: {
×
458
      ...workingArticle,
×
459
      bibliography: { text: bibliography, entries: bibTeXEntries },
×
460
    },
×
461
  }
×
462
}
×
463

464
function setWorkingArticleState(state, { workingArticleState, message }) {
×
465
  const { workingArticle } = state
×
466
  return {
×
467
    ...state,
×
468
    workingArticle: {
×
469
      ...workingArticle,
×
470
      state: workingArticleState,
×
471
      stateMessage: message,
×
472
    },
×
473
  }
×
474
}
×
475

476
function togglePreferences(storeKey) {
22✔
477
  return function togglePreferencesReducer(state, { key, value }) {
22✔
478
    const preferences = state[storeKey]
×
479

480
    return {
×
481
      ...state,
×
482
      [storeKey]: {
×
483
        ...preferences,
×
484
        [key]: value === undefined ? !preferences[key] : value,
×
485
      },
×
486
    }
×
487
  }
×
488
}
22✔
489

490
function setPreferences(storeKey) {
11✔
491
  return function setPreferencesReducer(state, { key, value }) {
11✔
492
    const preferences = state[storeKey]
×
493

494
    return {
×
495
      ...state,
×
496
      [storeKey]: {
×
497
        ...preferences,
×
498
        [key]: value,
×
499
      },
×
500
    }
×
501
  }
×
502
}
11✔
503

504
const toggleArticlePreferences = togglePreferences('articlePreferences')
1✔
505
const toggleUserPreferences = togglePreferences('userPreferences')
1✔
506
const setExportPreferences = setPreferences('exportPreferences')
1✔
507

508
function updateEditorCursorPosition(state, { lineNumber, column }) {
×
509
  return {
×
510
    ...state,
×
511
    editorCursorPosition: {
×
512
      lineNumber,
×
513
      column,
×
514
    },
×
515
  }
×
516
}
×
517

518
function setWorkspaces(state, { workspaces }) {
×
519
  return {
×
520
    ...state,
×
521
    activeUser: {
×
522
      ...state.activeUser,
×
523
      workspaces,
×
524
    },
×
525
  }
×
526
}
×
527

528
function setActiveWorkspace(state, { workspaceId }) {
×
529
  return {
×
530
    ...state,
×
531
    activeUser: {
×
532
      ...state.activeUser,
×
533
      activeWorkspaceId: workspaceId,
×
534
    },
×
535
  }
×
536
}
×
537

538
function updateSelectedTag(state, { tagId }) {
×
539
  const { selectedTagIds } = state.activeUser
×
540
  return {
×
541
    ...state,
×
542
    activeUser: {
×
543
      ...state.activeUser,
×
544
      selectedTagIds: selectedTagIds.includes(tagId)
×
545
        ? selectedTagIds.filter((selectedTagId) => selectedTagId !== tagId)
×
546
        : [...selectedTagIds, tagId],
×
547
    },
×
548
  }
×
549
}
×
550

551
function tagCreated(state, { tag }) {
×
552
  return {
×
553
    ...state,
×
554
    latestTagCreated: tag,
×
555
  }
×
556
}
×
557

558
function setLatestCorpusDeleted(state, { data }) {
×
559
  return {
×
560
    ...state,
×
561
    latestCorpusDeleted: data,
×
562
  }
×
563
}
×
564

565
function setLatestCorpusCreated(state, { data }) {
×
566
  return {
×
567
    ...state,
×
568
    latestCorpusCreated: data,
×
569
  }
×
570
}
×
571

572
function setLatestCorpusUpdated(state, { data }) {
×
573
  return {
×
574
    ...state,
×
575
    latestCorpusUpdated: data,
×
576
  }
×
577
}
×
578

579
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
1✔
580

581
export default function createReduxStore(state = initialState) {
1✔
582
  return createStore(
6✔
583
    createRootReducer(state),
6✔
584
    composeEnhancers(
6✔
585
      applyMiddleware(createNewArticleVersion, persistStateIntoLocalStorage),
6✔
586
      sentryReduxEnhancer
6✔
587
    )
6✔
588
  )
6✔
589
}
6✔
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