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

EcrituresNumeriques / stylo / 21433867219

28 Jan 2026 10:06AM UTC coverage: 69.336% (-0.5%) from 69.865%
21433867219

Pull #1870

github

web-flow
Merge 2ecb40001 into 3e2a2d1b3
Pull Request #1870: Redesign de l'écran articles

731 of 1151 branches covered (63.51%)

Branch coverage included in aggregate %.

30 of 107 new or added lines in 4 files covered. (28.04%)

36 existing lines in 2 files now uncovered.

4013 of 5691 relevant lines covered (70.51%)

5.8 hits per line

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

31.25
/front/src/hooks/article.js
1
import { merge } from 'allof-merge'
2
import { useSelector } from 'react-redux'
3

4
import { toYaml } from '../components/Write/metadata/yaml.js'
5
import { toEntries } from '../helpers/bibtex.js'
6
import { executeQuery } from '../helpers/graphQL.js'
7
import { clean } from '../schemas/schemas.js'
8
import useFetchData, {
9
  useConditionalFetchData,
10
  useMutateData,
11
} from './graphql.js'
12

13
import {
14
  addTags,
15
  deleteArticle,
16
  duplicateArticle,
17
  getArticleMetadata,
18
  getArticleTags,
19
  getEditableArticle,
20
  removeTags,
21
  renameArticle,
22
  updateWorkingVersion,
23
  updateZoteroLinkMutation,
24
} from './Article.graphql'
25
import {
26
  createVersion,
27
  getArticleVersion,
28
  getArticleVersions,
29
  renameVersion,
30
} from './Versions.graphql'
31

32
export function useArticleTagActions({ articleId }) {
33
  const sessionToken = useSelector((state) => state.sessionToken)
8✔
34
  const { data, mutate, error, isLoading } = useFetchData(
6✔
35
    { query: getArticleTags, variables: { articleId } },
36
    {
37
      revalidateOnFocus: false,
38
      revalidateOnReconnect: false,
39
    }
40
  )
41
  const add = async (tagId) => {
6✔
42
    const result = await executeQuery({
1✔
43
      query: addTags,
44
      variables: {
45
        articleId,
46
        tags: [tagId],
47
      },
48
      sessionToken,
49
      type: 'mutation',
50
    })
51
    const tags = result.article.addTags
1✔
52
    mutate(
1✔
53
      {
54
        article: {
55
          tags,
56
        },
57
      },
58
      { revalidate: false }
59
    )
60
    return tags
1✔
61
  }
62
  const remove = async (tagId) => {
6✔
63
    const result = await executeQuery({
1✔
64
      query: removeTags,
65
      variables: {
66
        articleId,
67
        tags: [tagId],
68
      },
69
      sessionToken,
70
      type: 'mutation',
71
    })
72
    const tags = result.article.removeTags
1✔
73
    mutate(
1✔
74
      {
75
        article: {
76
          tags,
77
        },
78
      },
79
      { revalidate: false }
80
    )
81
    return tags
1✔
82
  }
83

84
  return {
6✔
85
    tags: data?.article?.tags || [],
10✔
86
    isLoading,
87
    error,
88
    add,
89
    remove,
90
  }
91
}
92

93
export function useArticleActions({ articleId }) {
94
  const sessionToken = useSelector((state) => state.sessionToken)
8✔
95
  const activeUser = useSelector((state) => state.activeUser)
8✔
96
  const copy = async (toUserId) => {
4✔
97
    return await executeQuery({
1✔
98
      query: duplicateArticle,
99
      variables: {
100
        user: null,
101
        to: toUserId,
102
        articleId,
103
      },
104
      sessionToken,
105
      type: 'mutation',
106
    })
107
  }
108
  const duplicate = async () => {
4✔
109
    return await executeQuery({
1✔
110
      query: duplicateArticle,
111
      variables: {
112
        user: activeUser._id,
113
        to: activeUser._id,
114
        articleId,
115
      },
116
      sessionToken,
117
      type: 'mutation',
118
    })
119
  }
120
  const rename = async (title) => {
4✔
121
    return await executeQuery({
1✔
122
      query: renameArticle,
123
      variables: { user: activeUser._id, articleId, title },
124
      sessionToken,
125
      type: 'mutation',
126
    })
127
  }
128
  const remove = async () => {
4✔
129
    return await executeQuery({
1✔
130
      query: deleteArticle,
131
      variables: { articleId },
132
      sessionToken,
133
      type: 'mutation',
134
    })
135
  }
136

137
  return {
4✔
138
    copy,
139
    duplicate,
140
    rename,
141
    remove,
142
  }
143
}
144

145
const emptyYaml = `---
1✔
146
---`
147

148
export function useArticleMetadata({ articleId, versionId }) {
UNCOV
149
  const sessionToken = useSelector((state) => state.sessionToken)
×
UNCOV
150
  const activeUser = useSelector((state) => state.activeUser)
×
UNCOV
151
  const hasVersion = typeof versionId === 'string'
×
UNCOV
152
  const { data, error, mutate, isLoading } = useFetchData(
×
153
    {
154
      query: getArticleMetadata,
155
      variables: { articleId, versionId: versionId ?? '', hasVersion },
×
156
    },
157
    {
158
      revalidateOnFocus: false,
159
      revalidateOnReconnect: false,
160
    }
161
  )
UNCOV
162
  const { mutate: mutateEditableArticle } = useMutateData({
×
163
    query: getEditableArticle,
164
    variables: {
165
      article: articleId,
166
      hasVersion,
167
      version: versionId ?? '',
×
168
    },
169
  })
170

NEW
171
  const updateMetadataFormType = async (metadataFormType) => {
×
NEW
172
    if (versionId) {
×
NEW
173
      return
×
174
    }
175

NEW
176
    await executeQuery({
×
177
      sessionToken,
178
      query: updateWorkingVersion,
179
      variables: {
180
        userId: activeUser._id,
181
        articleId: articleId,
182
        content: { metadataFormType },
183
      },
184
      type: 'mutate',
185
    })
186
    // TODO use a common query for all mutations
UNCOV
187
    await mutate(
×
188
      async (data) => {
NEW
189
        return {
×
190
          article: {
191
            ...data.article,
192
            workingVersion: {
193
              ...data.article.workingVersion,
194
              metadataFormType: metadataFormType,
195
            },
196
          },
197
        }
198
      },
199
      { revalidate: false }
200
    )
201
  }
202

NEW
203
  const updateMetadata = async (metadata) => {
×
NEW
204
    if (versionId) {
×
NEW
205
      return
×
206
    }
NEW
207
    await executeQuery({
×
208
      sessionToken,
209
      query: updateWorkingVersion,
210
      variables: {
211
        userId: activeUser._id,
212
        articleId: articleId,
213
        content: { metadata },
214
      },
215
      type: 'mutate',
216
    })
NEW
217
    await mutate(
×
218
      async (data) => {
NEW
219
        return {
×
220
          article: {
221
            ...data.article,
222
            workingVersion: {
223
              ...data.article.workingVersion,
224
              metadata: metadata,
225
            },
226
          },
227
        }
228
      },
229
      { revalidate: false }
230
    )
NEW
231
    await mutateEditableArticle()
×
232
  }
233

NEW
234
  const metadata = hasVersion
×
235
    ? data?.version?.metadata
236
    : data?.article?.workingVersion?.metadata
237

NEW
238
  const options = data?.article?.workspaces
×
UNCOV
239
    ?.filter((w) => w.formMetadata.data !== null)
×
240
    .map((w) => {
UNCOV
241
      try {
×
UNCOV
242
        const data = merge(JSON.parse(w.formMetadata.data))
×
243
        const ui =
UNCOV
244
          w.formMetadata.ui !== null ? JSON.parse(w.formMetadata.ui) : {}
×
NEW
245
        return {
×
246
          name: data.title || 'untitled',
×
247
          data,
248
          ui,
249
        }
250
      } catch (e) {
UNCOV
251
        console.error(
×
252
          `Ignore form metadata configured on workspace id: ${w._id}`,
253
          e
254
        )
255
        return null
×
256
      }
257
    })
UNCOV
258
    ?.filter((o) => o !== null)
×
259

UNCOV
260
  const metadataFormType = hasVersion
×
261
    ? data?.version?.metadataFormType
262
    : data?.article?.workingVersion?.metadataFormType
263

UNCOV
264
  const metadataYaml = metadata ? toYaml(clean(metadata)) : emptyYaml
×
UNCOV
265
  return {
×
266
    metadata,
267
    metadataFormType,
268
    metadataFormTypeOptions: options ?? [],
×
269
    metadataYaml: metadataYaml === '' ? emptyYaml : metadataYaml,
×
270
    updateMetadata,
271
    updateMetadataFormType,
272
    isLoading,
273
    error,
274
  }
275
}
276

277
export function useEditableArticle({ articleId, versionId }) {
UNCOV
278
  const sessionToken = useSelector((state) => state.sessionToken)
×
UNCOV
279
  const activeUser = useSelector((state) => state.activeUser)
×
280
  const hasVersion = typeof versionId === 'string'
×
UNCOV
281
  const { data, mutate, error, isLoading } = useFetchData(
×
282
    {
283
      query: getEditableArticle,
284
      variables: {
285
        article: articleId,
286
        hasVersion,
287
        version: versionId ?? '',
×
288
      },
289
    },
290
    {
291
      revalidateOnFocus: false,
292
      revalidateOnReconnect: false,
293
      fallbackData: {
294
        article: {},
295
      },
296
    }
297
  )
298

UNCOV
299
  const updateBibliography = async (bib) => {
×
UNCOV
300
    if (hasVersion) {
×
301
      // can only update the bibliography on the working copy
UNCOV
302
      return
×
303
    }
UNCOV
304
    await executeQuery({
×
305
      sessionToken,
306
      query: updateWorkingVersion,
307
      variables: {
308
        userId: activeUser._id,
309
        articleId: articleId,
310
        content: { bib },
311
      },
312
      type: 'mutate',
313
    })
UNCOV
314
    await mutate(
×
315
      async (data) => {
UNCOV
316
        return {
×
317
          article: {
318
            ...data.article,
319
            workingVersion: {
320
              ...data.article.workingVersion,
321
              bib,
322
            },
323
          },
324
        }
325
      },
326
      { revalidate: false }
327
    )
328
  }
329

UNCOV
330
  const updateZoteroLink = async (url) => {
×
UNCOV
331
    await executeQuery({
×
332
      sessionToken,
333
      query: updateZoteroLinkMutation,
334
      variables: {
335
        articleId: articleId,
336
        url,
337
      },
338
      type: 'mutate',
339
    })
UNCOV
340
    await mutate(
×
341
      async (data) => {
342
        return {
×
343
          article: {
344
            ...data.article,
345
            zoteroLink: url,
346
          },
347
        }
348
      },
349
      { revalidate: false }
350
    )
351
  }
352

UNCOV
353
  const bibtext = hasVersion
×
354
    ? (data?.article?.version?.bib ?? '')
×
355
    : (data?.article?.workingVersion?.bib ?? '')
×
356

UNCOV
357
  const entries = toEntries(bibtext)
×
358

359
  return {
×
360
    article: data?.article,
361
    updateBibliography,
362
    updateZoteroLink,
363
    bibliography: {
364
      bibtext,
365
      entries,
366
    },
367
    isLoading,
368
    error,
369
  }
370
}
371

372
export function useArticleVersions({ articleId }) {
UNCOV
373
  const { data, error, isLoading } = useFetchData(
×
374
    { query: getArticleVersions, variables: { article: articleId } },
375
    {
376
      revalidateOnFocus: false,
377
      revalidateOnReconnect: false,
378
    }
379
  )
380

UNCOV
381
  return {
×
382
    error,
383
    isLoading,
384
    article: data?.article,
385
  }
386
}
387

388
export function useArticleVersion({ versionId }) {
UNCOV
389
  const { data, error, isLoading } = useConditionalFetchData(
×
390
    versionId ? { query: getArticleVersion, variables: { versionId } } : null,
×
391
    {
392
      revalidateOnFocus: false,
393
      revalidateOnReconnect: false,
394
      fallbackData: {
395
        version: {},
396
      },
397
    }
398
  )
399

UNCOV
400
  return {
×
401
    error,
402
    isLoading,
403
    version: data?.version,
404
  }
405
}
406

407
export function useArticleVersionActions({ articleId }) {
408
  const sessionToken = useSelector((state) => state.sessionToken)
4✔
409
  const activeUser = useSelector((state) => state.activeUser)
4✔
410
  const { mutate } = useMutateData({
2✔
411
    query: getArticleVersions,
412
    variables: { article: articleId },
413
  })
414
  const create = async (version) => {
2✔
415
    const response = await executeQuery({
1✔
416
      query: createVersion,
417
      variables: {
418
        userId: activeUser._id,
419
        articleId,
420
        major: version.major,
421
        message: version.description,
422
      },
423
      sessionToken,
424
      type: 'mutation',
425
    })
426
    await mutate(async (data) => ({
1✔
427
      article: {
428
        ...data?.article,
429
        versions: response.article.createVersion.versions,
430
      },
431
    }))
432
  }
433
  const updateDescription = async ({ versionId, description }) => {
2✔
434
    await executeQuery({
1✔
435
      query: renameVersion,
436
      variables: {
437
        version: versionId,
438
        name: description,
439
      },
440
      sessionToken,
441
      type: 'mutation',
442
    })
443
    await mutate(async (data) => ({
1✔
444
      article: {
445
        ...data?.article,
446
        versions: data.article.versions.map((v) => {
447
          if (v._id === versionId) {
2!
UNCOV
448
            return {
×
449
              ...v,
450
              message: description,
451
            }
452
          }
453
          return v
2✔
454
        }),
455
      },
456
    }))
457
  }
458

459
  return {
2✔
460
    create,
461
    updateDescription,
462
  }
463
}
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