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

EcrituresNumeriques / stylo / 12909556719

22 Jan 2025 01:45PM UTC coverage: 30.526% (+0.04%) from 30.49%
12909556719

push

github

web-flow
Merge pull request #1174 from EcrituresNumeriques/feat/1067

390 of 587 branches covered (66.44%)

Branch coverage included in aggregate %.

49 of 162 new or added lines in 9 files covered. (30.25%)

72 existing lines in 4 files now uncovered.

4069 of 14020 relevant lines covered (29.02%)

1.7 hits per line

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

25.38
/front/src/createReduxStore.js
1
import { applyMiddleware, compose, createStore } from 'redux'
1✔
2
import * as Sentry from '@sentry/react'
3
import { toEntries } from './helpers/bibtex'
4
import ArticleService from './services/ArticleService'
5
import WorkspaceService from './services/WorkspaceService.js'
6
import { applicationConfig } from './config.js'
7
const { SNOWPACK_SESSION_STORAGE_ID: sessionTokenName = 'sessionToken' } =
1✔
8
  import.meta.env
1✔
9

10
const sentryReduxEnhancer = Sentry.createReduxEnhancer()
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
  editorCursorPosition: {
1✔
75
    lineNumber: 0,
1✔
76
    column: 0,
1✔
77
  },
1✔
78
}
1✔
79

80
function createRootReducer(state) {
6✔
81
  return createReducer(state, {
6✔
82
    PROFILE: setProfile,
6✔
83
    CLEAR_ZOTERO_TOKEN: clearZoteroToken,
6✔
84
    LOGIN: loginUser,
6✔
85
    UPDATE_SESSION_TOKEN: setSessionToken,
6✔
86
    UPDATE_ACTIVE_USER_DETAILS: updateActiveUserDetails,
6✔
87
    LOGOUT: logoutUser,
6✔
88

89
    // article reducers
6✔
90
    UPDATE_ARTICLE_STATS: updateArticleStats,
6✔
91
    UPDATE_ARTICLE_STRUCTURE: updateArticleStructure,
6✔
92
    UPDATE_ARTICLE_WRITERS: updateArticleWriters,
6✔
93

94
    // user preferences reducers
6✔
95
    USER_PREFERENCES_TOGGLE: toggleUserPreferences,
6✔
96

97
    SET_ARTICLE_VERSIONS: setArticleVersions,
6✔
98
    SET_WORKING_ARTICLE_UPDATED_AT: setWorkingArticleUpdatedAt,
6✔
99
    SET_WORKING_ARTICLE_TEXT: setWorkingArticleText,
6✔
100
    SET_WORKING_ARTICLE_METADATA: setWorkingArticleMetadata,
6✔
101
    SET_WORKING_ARTICLE_BIBLIOGRAPHY: setWorkingArticleBibliography,
6✔
102
    SET_WORKING_ARTICLE_STATE: setWorkingArticleState,
6✔
103
    SET_CREATE_ARTICLE_VERSION_ERROR: setCreateArticleVersionError,
6✔
104

105
    ARTICLE_PREFERENCES_TOGGLE: toggleArticlePreferences,
6✔
106

107
    UPDATE_EDITOR_CURSOR_POSITION: updateEditorCursorPosition,
6✔
108

109
    SET_WORKSPACES: setWorkspaces,
6✔
110
    SET_ACTIVE_WORKSPACE: setActiveWorkspace,
6✔
111

112
    UPDATE_SELECTED_TAG: updateSelectedTag,
6✔
113
    TAG_CREATED: tagCreated,
6✔
114

115
    SET_LATEST_CORPUS_DELETED: setLatestCorpusDeleted,
6✔
116
    SET_LATEST_CORPUS_CREATED: setLatestCorpusCreated,
6✔
117
    SET_LATEST_CORPUS_UPDATED: setLatestCorpusUpdated,
6✔
118
  })
6✔
119
}
6✔
120

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

260
function persistStateIntoLocalStorage({ getState }) {
6✔
261
  return (next) => {
6✔
262
    return (action) => {
6✔
263
      if (action.type === 'ARTICLE_PREFERENCES_TOGGLE') {
×
264
        // we run the reducer first
×
265
        next(action)
×
266
        // we fetch the updated state
×
267
        const { articlePreferences } = getState()
×
268
        // we persist it for a later page reload
×
UNCOV
269
        localStorage.setItem(
×
UNCOV
270
          'articlePreferences',
×
UNCOV
271
          JSON.stringify(articlePreferences)
×
UNCOV
272
        )
×
273

NEW
274
        return
×
NEW
275
      } else if (action.type === 'USER_PREFERENCES_TOGGLE') {
×
NEW
276
        // we run the reducer first
×
NEW
277
        next(action)
×
NEW
278
        // we fetch the updated state
×
UNCOV
279
        const { userPreferences } = getState()
×
UNCOV
280
        // we persist it for a later page reload
×
NEW
281
        localStorage.setItem('userPreferences', JSON.stringify(userPreferences))
×
282

283
        return
×
284
      } else if (action.type === 'LOGOUT') {
×
285
        const { backendEndpoint } = applicationConfig
×
NEW
286
        localStorage.removeItem('articlePreferences')
×
287
        localStorage.removeItem('userPreferences')
×
288
        document.location.replace(backendEndpoint + '/logout')
×
NEW
289
      }
×
290

291
      if (action.type === 'LOGIN' || action.type === 'UPDATE_SESSION_TOKEN') {
×
292
        next(action)
×
293
        const { sessionToken } = getState()
×
294
        localStorage.setItem(sessionTokenName, sessionToken)
×
295
        return
×
296
      }
×
297

UNCOV
298
      if (action.type === 'LOGOUT') {
×
299
        localStorage.removeItem(sessionTokenName)
×
300
        return next(action)
×
301
      }
×
302

303
      return next(action)
×
304
    }
×
305
  }
6✔
306
}
6✔
307

308
function setProfile(state, action) {
×
309
  const { user } = action
×
UNCOV
310
  if (!user) {
×
311
    return { ...state, activeUser: undefined, hasBooted: true }
×
312
  }
×
UNCOV
313
  return {
×
UNCOV
314
    ...state,
×
UNCOV
315
    hasBooted: true,
×
316
    loggedIn: true,
×
317
    activeUser: {
×
318
      ...state.activeUser,
×
319
      activeWorkspaceId: action.activeWorkspaceId,
×
320
      ...user,
×
321
    },
×
322
  }
×
323
}
×
324

325
function clearZoteroToken(state) {
×
326
  return {
×
327
    ...state,
×
328
    activeUser: {
×
329
      ...state.activeUser,
×
330
      zoteroToken: null,
×
331
    },
×
UNCOV
332
  }
×
333
}
×
334

335
function setSessionToken(state, { token: sessionToken }) {
×
336
  return {
×
337
    ...state,
×
338
    sessionToken,
×
339
  }
×
340
}
×
341

UNCOV
342
function loginUser(state, { user, token: sessionToken }) {
×
343
  if (sessionToken) {
×
344
    Sentry.setUser({ id: user._id })
×
345
    return {
×
346
      ...state,
×
347
      sessionToken,
×
348
      activeUser: {
×
UNCOV
349
        ...state.user,
×
350
        ...user,
×
351
        // dates are expected to be in timestamp string format (including milliseconds)
×
352
        createdAt: String(new Date(user.createdAt).getTime()),
×
353
        updatedAt: String(new Date(user.updatedAt).getTime()),
×
354
      },
×
355
    }
×
356
  }
×
357

358
  return state
×
359
}
×
360

361
function updateActiveUserDetails(state, action) {
×
362
  return {
×
363
    ...state,
×
364
    activeUser: { ...state.activeUser, ...action.payload },
×
UNCOV
365
  }
×
366
}
×
367

UNCOV
368
function logoutUser(state) {
×
369
  Sentry.setUser(null)
×
370
  return { ...state, ...initialState }
×
371
}
×
372

373
const SPACE_RE = /\s+/gi
1✔
374
const CITATION_RE = /(\[@[\w-]+)/gi
1✔
375
const REMOVE_MARKDOWN_RE = /[#_*]+\s?/gi
1✔
376

377
function updateArticleStats(state, { md }) {
1✔
378
  const text = (md || '').trim()
×
379

UNCOV
380
  const textWithoutMarkdown = text.replace(REMOVE_MARKDOWN_RE, '')
×
UNCOV
381
  const wordCount = textWithoutMarkdown.replace(SPACE_RE, ' ').split(' ').length
×
382

UNCOV
383
  const charCountNoSpace = textWithoutMarkdown.replace(SPACE_RE, '').length
×
UNCOV
384
  const charCountPlusSpace = textWithoutMarkdown.length
×
UNCOV
385
  const citationNb = text.match(CITATION_RE)?.length || 0
×
386

UNCOV
387
  return {
×
388
    ...state,
×
389
    articleStats: {
×
UNCOV
390
      wordCount,
×
391
      charCountNoSpace,
×
392
      charCountPlusSpace,
×
393
      citationNb,
×
UNCOV
394
    },
×
395
  }
×
396
}
×
397

398
function updateArticleStructure(state, { md }) {
×
399
  const text = (md || '').trim()
×
400
  const articleStructure = text
×
401
    .split('\n')
×
402
    .map((line, index) => ({ line, index }))
×
403
    .filter((lineWithIndex) => lineWithIndex.line.match(/^##+ /))
×
404
    .map((lineWithIndex) => {
×
UNCOV
405
      const title = lineWithIndex.line
×
406
        .replace(/##/, '')
×
407
        //arrow backspace (\u21B3)
×
408
        .replace(/#\s/g, '\u21B3')
×
409
        // middle dot (\u00B7) + non-breaking space (\xa0)
×
410
        .replace(/#/g, '\u00B7\xa0')
×
411
      return { ...lineWithIndex, title }
×
412
    })
×
413

414
  return { ...state, articleStructure }
×
415
}
×
416

417
function updateArticleWriters(state, { articleWriters }) {
×
418
  return { ...state, articleWriters }
×
419
}
×
420

UNCOV
421
function setArticleVersions(state, { versions }) {
×
422
  return { ...state, articleVersions: versions }
×
423
}
×
424

425
function setCreateArticleVersionError(state, { err }) {
×
426
  return { ...state, createArticleVersionError: err }
×
427
}
×
428

429
function setWorkingArticleUpdatedAt(state, { updatedAt }) {
×
430
  const { workingArticle } = state
×
431
  return { ...state, workingArticle: { ...workingArticle, updatedAt } }
×
UNCOV
432
}
×
433

434
function setWorkingArticleText(state, { text }) {
×
435
  const { workingArticle } = state
×
UNCOV
436
  return { ...state, workingArticle: { ...workingArticle, text } }
×
437
}
×
438

439
function setWorkingArticleMetadata(state, { metadata }) {
×
440
  const { workingArticle } = state
×
UNCOV
441
  return { ...state, workingArticle: { ...workingArticle, metadata } }
×
442
}
×
443

444
function setWorkingArticleBibliography(state, { bibliography }) {
×
445
  const bibTeXEntries = toEntries(bibliography)
×
UNCOV
446
  const { workingArticle } = state
×
447
  return {
×
448
    ...state,
×
449
    workingArticle: {
×
450
      ...workingArticle,
×
UNCOV
451
      bibliography: { text: bibliography, entries: bibTeXEntries },
×
452
    },
×
453
  }
×
454
}
×
455

456
function setWorkingArticleState(state, { workingArticleState, message }) {
×
457
  const { workingArticle } = state
×
458
  return {
×
459
    ...state,
×
460
    workingArticle: {
×
461
      ...workingArticle,
×
462
      state: workingArticleState,
×
UNCOV
463
      stateMessage: message,
×
464
    },
×
465
  }
×
466
}
×
467

468
function toggleArticlePreferences(state, { key, value }) {
×
469
  const { articlePreferences } = state
×
470

471
  return {
×
472
    ...state,
×
473
    articlePreferences: {
×
474
      ...articlePreferences,
×
UNCOV
475
      [key]: value === undefined ? !articlePreferences[key] : value,
×
NEW
476
    },
×
NEW
477
  }
×
NEW
478
}
×
479

NEW
480
function toggleUserPreferences(state, { key, value }) {
×
NEW
481
  const { userPreferences } = state
×
482

NEW
483
  return {
×
NEW
484
    ...state,
×
NEW
485
    userPreferences: {
×
NEW
486
      ...userPreferences,
×
487
      [key]: value === undefined ? !userPreferences[key] : value,
×
488
    },
×
UNCOV
489
  }
×
NEW
490
}
×
491

NEW
492
function updateEditorCursorPosition(state, { lineNumber, column }) {
×
493
  return {
×
NEW
494
    ...state,
×
NEW
495
    editorCursorPosition: {
×
NEW
496
      lineNumber,
×
NEW
497
      column,
×
NEW
498
    },
×
NEW
499
  }
×
NEW
500
}
×
501

502
function setWorkspaces(state, { workspaces }) {
×
UNCOV
503
  return {
×
NEW
504
    ...state,
×
NEW
505
    activeUser: {
×
NEW
506
      ...state.activeUser,
×
NEW
507
      workspaces,
×
508
    },
×
509
  }
×
510
}
×
511

512
function setActiveWorkspace(state, { workspaceId }) {
×
513
  return {
×
514
    ...state,
×
515
    activeUser: {
×
516
      ...state.activeUser,
×
UNCOV
517
      activeWorkspaceId: workspaceId,
×
518
    },
×
519
  }
×
520
}
×
521

522
function updateSelectedTag(state, { tagId }) {
×
523
  const { selectedTagIds } = state.activeUser
×
524
  return {
×
525
    ...state,
×
526
    activeUser: {
×
UNCOV
527
      ...state.activeUser,
×
528
      selectedTagIds: selectedTagIds.includes(tagId)
×
529
        ? selectedTagIds.filter((selectedTagId) => selectedTagId !== tagId)
×
530
        : [...selectedTagIds, tagId],
×
531
    },
×
532
  }
×
533
}
×
534

535
function tagCreated(state, { tag }) {
×
536
  return {
×
UNCOV
537
    ...state,
×
538
    latestTagCreated: tag,
×
539
  }
×
540
}
×
541

542
function setLatestCorpusDeleted(state, { data }) {
×
543
  return {
×
544
    ...state,
×
545
    latestCorpusDeleted: data,
×
546
  }
×
547
}
×
548

549
function setLatestCorpusCreated(state, { data }) {
×
UNCOV
550
  return {
×
551
    ...state,
×
552
    latestCorpusCreated: data,
×
553
  }
×
554
}
×
555

556
function setLatestCorpusUpdated(state, { data }) {
×
UNCOV
557
  return {
×
558
    ...state,
×
559
    latestCorpusUpdated: data,
×
560
  }
×
561
}
×
562

563
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
1✔
564

565
export default function createReduxStore(state = initialState) {
1✔
566
  return createStore(
6✔
567
    createRootReducer(state),
6✔
568
    composeEnhancers(
6✔
569
      applyMiddleware(createNewArticleVersion, persistStateIntoLocalStorage),
6✔
570
      sentryReduxEnhancer
6✔
571
    )
6✔
572
  )
6✔
573
}
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