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

EcrituresNumeriques / stylo / 15710635963

17 Jun 2025 02:50PM UTC coverage: 39.506% (-0.1%) from 39.645%
15710635963

Pull #1607

github

web-flow
Merge de835e1f4 into 5f2fdcf95
Pull Request #1607: Permet d'éviter le décalage entre les pages qui ont une scrollbar et celles qui n'en ont pas

574 of 802 branches covered (71.57%)

Branch coverage included in aggregate %.

5615 of 14864 relevant lines covered (37.78%)

2.67 hits per line

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

57.52
/front/src/createReduxStore.js
1
import { createReduxEnhancer as createSentryReduxEnhancer } from '@sentry/react'
1✔
2

3
import { applyMiddleware, compose, createStore } from 'redux'
1✔
4

5
import { computeTextStats } from './helpers/markdown.js'
1✔
6

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
  articleWorkingCopy: {
1✔
13
    status: 'synced',
1✔
14
  },
1✔
15
  articleStructure: [],
1✔
16
  articleWriters: [],
1✔
17
  articlePreferences: localStorage.getItem('articlePreferences')
1✔
18
    ? JSON.parse(localStorage.getItem('articlePreferences'))
1!
19
    : {
1✔
20
        expandSidebarRight: true,
1✔
21
        activePanel: null,
1✔
22
        metadataFormMode: 'basic',
1✔
23
      },
1✔
24
  articleFilters: {
1✔
25
    tagIds: [],
1✔
26
    text: '',
1✔
27
  },
1✔
28
  articleStats: {
1✔
29
    wordCount: 0,
1✔
30
    charCountNoSpace: 0,
1✔
31
    charCountPlusSpace: 0,
1✔
32
    citationNb: 0,
1✔
33
  },
1✔
34
  // Active user (authenticated)
1✔
35
  activeUser: {
1✔
36
    authTypes: [],
1✔
37
    authProviders: {},
1✔
38
    selectedTagIds: [],
1✔
39
    workspaces: [],
1✔
40
  },
1✔
41
  userPreferences: localStorage.getItem('userPreferences')
1✔
42
    ? JSON.parse(localStorage.getItem('userPreferences'))
1!
43
    : {
1✔
44
        trackingConsent: true /* default value should be false */,
1✔
45
      },
1✔
46
  exportPreferences: localStorage.getItem('exportPreferences')
1✔
47
    ? JSON.parse(localStorage.getItem('exportPreferences'))
1!
48
    : {
1✔
49
        bibliography_style: 'chicagomodified',
1✔
50
        with_toc: 0,
1✔
51
        link_citations: 0,
1✔
52
        with_nocite: 0,
1✔
53
        formats: 'html',
1✔
54
        unnumbered: 0,
1✔
55
        book_division: 'part',
1✔
56
      },
1✔
57
  editorCursorPosition: {
1✔
58
    lineNumber: 0,
1✔
59
    column: 0,
1✔
60
  },
1✔
61
}
1✔
62

63
/**
1✔
64
 *
1✔
65
 * @param {*} state
1✔
66
 * @param initialState
1✔
67
 * @param handlers
1✔
68
 * @returns
1✔
69
 */
1✔
70
function createReducer(initialState, handlers) {
28✔
71
  return function reducer(state = initialState, action) {
28✔
72
    if (Object.prototype.hasOwnProperty.call(handlers, action.type)) {
31✔
73
      return handlers[action.type](state, action)
3✔
74
    } else {
31✔
75
      return state
28✔
76
    }
28✔
77
  }
31✔
78
}
28✔
79

80
/**
1✔
81
 *
1✔
82
 * @param {*} state
1✔
83
 * @returns
1✔
84
 */
1✔
85
function createRootReducer(state) {
28✔
86
  return createReducer(state, {
28✔
87
    PROFILE: setProfile,
28✔
88
    LOGIN: loginUser,
28✔
89
    UPDATE_SESSION_TOKEN: setSessionToken,
28✔
90
    UPDATE_ACTIVE_USER_DETAILS: updateActiveUserDetails,
28✔
91
    LOGOUT: logoutUser,
28✔
92

93
    // article reducers
28✔
94
    UPDATE_ARTICLE_STATS: updateArticleStats,
28✔
95
    UPDATE_ARTICLE_STRUCTURE: updateArticleStructure,
28✔
96
    UPDATE_ARTICLE_WRITERS: updateArticleWriters,
28✔
97
    UPDATE_ARTICLE_WORKING_COPY_STATUS: updateArticleWorkingCopyStatus,
28✔
98

99
    // user preferences reducers
28✔
100
    USER_PREFERENCES_TOGGLE: toggleUserPreferences,
28✔
101
    ARTICLE_PREFERENCES_TOGGLE: toggleArticlePreferences,
28✔
102
    CORPUS_PREFERENCES_TOGGLE: toggleCorpusPreferences,
28✔
103

104
    SET_ARTICLE_PREFERENCES: setArticlePreferences,
28✔
105
    SET_EXPORT_PREFERENCES: setExportPreferences,
28✔
106
    SET_CORPUS_PREFERENCES: setCorpusPreferences,
28✔
107

108
    UPDATE_EDITOR_CURSOR_POSITION: updateEditorCursorPosition,
28✔
109

110
    UPDATE_SELECTED_TAG: updateSelectedTag,
28✔
111
  })
28✔
112
}
28✔
113

114
function persistStateIntoLocalStorage({ getState }) {
28✔
115
  const actionStateMap = new Map([
28✔
116
    ['ARTICLE_PREFERENCES_TOGGLE', 'articlePreferences'],
28✔
117
    ['SET_ARTICLE_PREFERENCES', 'articlePreferences'],
28✔
118
    ['USER_PREFERENCES_TOGGLE', 'userPreferences'],
28✔
119
    ['SET_EXPORT_PREFERENCES', 'exportPreferences'],
28✔
120
  ])
28✔
121

122
  return (next) => {
28✔
123
    return (action) => {
28✔
124
      if (actionStateMap.has(action.type)) {
3!
125
        const key = actionStateMap.get(action.type)
×
126
        // we run the reducer first
×
127
        next(action)
×
128
        // we fetch the updated state
×
129
        const state = getState()[key]
×
130

131
        // we persist it for a later page reload
×
132
        localStorage.setItem(key, JSON.stringify(state))
×
133

134
        return
×
135
      } else if (action.type === 'LOGOUT') {
3!
136
        localStorage.removeItem('articlePreferences')
×
137
        localStorage.removeItem('userPreferences')
×
138
      }
×
139

140
      if (action.type === 'LOGIN' || action.type === 'UPDATE_SESSION_TOKEN') {
3✔
141
        next(action)
1✔
142
        const { sessionToken } = getState()
1✔
143
        localStorage.setItem(sessionTokenName, sessionToken)
1✔
144
        return
1✔
145
      }
1✔
146

147
      if (action.type === 'LOGOUT') {
2!
148
        localStorage.removeItem(sessionTokenName)
×
149
        return next(action)
×
150
      }
×
151

152
      return next(action)
2✔
153
    }
3✔
154
  }
28✔
155
}
28✔
156

157
function setProfile(state, action) {
×
158
  const { user } = action
×
159

160
  if (!user) {
×
161
    return { ...state, activeUser: structuredClone(initialState.activeUser) }
×
162
  }
×
163

164
  return {
×
165
    ...state,
×
166
    activeUser: {
×
167
      ...state.activeUser,
×
168
      ...user,
×
169
    },
×
170
  }
×
171
}
×
172

173
function setSessionToken(state, { token: sessionToken }) {
×
174
  return {
×
175
    ...state,
×
176
    sessionToken,
×
177
  }
×
178
}
×
179

180
function loginUser(state, { user, token: sessionToken }) {
1✔
181
  if (sessionToken) {
1!
182
    return {
×
183
      ...state,
×
184
      sessionToken,
×
185
      activeUser: {
×
186
        ...state.activeUser,
×
187
        ...user,
×
188
        // dates are expected to be in timestamp string format (including milliseconds)
×
189
        createdAt: String(new Date(user.createdAt).getTime()),
×
190
        updatedAt: String(new Date(user.updatedAt).getTime()),
×
191
      },
×
192
    }
×
193
  }
×
194

195
  return state
1✔
196
}
1✔
197

198
function updateActiveUserDetails(state, action) {
2✔
199
  return {
2✔
200
    ...state,
2✔
201
    activeUser: { ...state.activeUser, ...action.payload },
2✔
202
  }
2✔
203
}
2✔
204

205
function logoutUser() {
×
206
  return structuredClone(initialState)
×
207
}
×
208

209
function updateArticleStats(state, { md }) {
×
210
  return {
×
211
    ...state,
×
212
    articleStats: computeTextStats(md),
×
213
  }
×
214
}
×
215

216
function updateArticleStructure(state, { md }) {
×
217
  const text = (md || '').trim()
×
218
  const articleStructure = text
×
219
    .split('\n')
×
220
    .map((line, index) => ({ line, index }))
×
221
    .filter((lineWithIndex) => lineWithIndex.line.match(/^##+ /))
×
222
    .map((lineWithIndex) => {
×
223
      const title = lineWithIndex.line
×
224
        .replace(/##/, '')
×
225
        //arrow backspace (\u21B3)
×
226
        .replace(/#\s/g, '\u21B3')
×
227
        // middle dot (\u00B7) + non-breaking space (\xa0)
×
228
        .replace(/#/g, '\u00B7\xa0')
×
229
      return { ...lineWithIndex, title }
×
230
    })
×
231

232
  return { ...state, articleStructure }
×
233
}
×
234

235
function updateArticleWriters(state, { articleWriters }) {
×
236
  return { ...state, articleWriters }
×
237
}
×
238

239
function updateArticleWorkingCopyStatus(state, { status }) {
×
240
  return {
×
241
    ...state,
×
242
    articleWorkingCopy: { ...state.articleWorkingCopy, status },
×
243
  }
×
244
}
×
245

246
function togglePreferences(storeKey) {
63✔
247
  return function togglePreferencesReducer(state, { key, value }) {
63✔
248
    const preferences = state[storeKey]
×
249

250
    return {
×
251
      ...state,
×
252
      [storeKey]: {
×
253
        ...preferences,
×
254
        [key]: value === undefined ? !preferences[key] : value,
×
255
      },
×
256
    }
×
257
  }
×
258
}
63✔
259

260
function setPreferences(storeKey) {
63✔
261
  return function setPreferencesReducer(state, { key, value }) {
63✔
262
    const preferences = state[storeKey]
×
263

264
    return {
×
265
      ...state,
×
266
      [storeKey]: {
×
267
        ...preferences,
×
268
        [key]: value,
×
269
      },
×
270
    }
×
271
  }
×
272
}
63✔
273

274
const toggleArticlePreferences = togglePreferences('articlePreferences')
1✔
275
const setArticlePreferences = setPreferences('articlePreferences')
1✔
276
const toggleUserPreferences = togglePreferences('userPreferences')
1✔
277
const toggleCorpusPreferences = togglePreferences('corpusPreferences')
1✔
278
const setExportPreferences = setPreferences('exportPreferences')
1✔
279
const setCorpusPreferences = setPreferences('corpusPreferences')
1✔
280

281
function updateEditorCursorPosition(state, { lineNumber, column }) {
×
282
  return {
×
283
    ...state,
×
284
    editorCursorPosition: {
×
285
      lineNumber,
×
286
      column,
×
287
    },
×
288
  }
×
289
}
×
290

291
function updateSelectedTag(state, { tagId }) {
×
292
  const { selectedTagIds } = state.activeUser
×
293
  return {
×
294
    ...state,
×
295
    activeUser: {
×
296
      ...state.activeUser,
×
297
      selectedTagIds: selectedTagIds.includes(tagId)
×
298
        ? selectedTagIds.filter((selectedTagId) => selectedTagId !== tagId)
×
299
        : [...selectedTagIds, tagId],
×
300
    },
×
301
  }
×
302
}
×
303

304
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
1✔
305
  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
1!
306
      trace: true,
×
307
      traceLimit: 25,
×
308
    })
×
309
  : compose
1✔
310

311
export default function createReduxStore(state = {}) {
1✔
312
  return createStore(
28✔
313
    createRootReducer({
28✔
314
      ...structuredClone(initialState),
28✔
315
      ...structuredClone(state),
28✔
316
    }),
28✔
317
    composeEnhancers(
28✔
318
      applyMiddleware(persistStateIntoLocalStorage),
28✔
319
      createSentryReduxEnhancer()
28✔
320
    )
28✔
321
  )
28✔
322
}
28✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc