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

EcrituresNumeriques / stylo / 15066626824

16 May 2025 10:41AM UTC coverage: 37.574% (-0.02%) from 37.594%
15066626824

Pull #1517

github

web-flow
Merge e05a1c169 into 607ffe7f9
Pull Request #1517: Mise à jour vers react-router@7

549 of 776 branches covered (70.75%)

Branch coverage included in aggregate %.

13 of 373 new or added lines in 28 files covered. (3.49%)

5 existing lines in 4 files now uncovered.

5319 of 14841 relevant lines covered (35.84%)

2.56 hits per line

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

0.0
/front/src/components/Article.jsx
1
import { useToasts } from '@geist-ui/core'
×
2
import clsx from 'clsx'
×
3
import {
×
4
  Check,
5
  ChevronDown,
6
  ChevronRight,
7
  Copy,
8
  Edit3,
9
  Eye,
10
  Pencil,
11
  Printer,
12
  Send,
13
  Trash,
14
  UserPlus,
15
} from 'lucide-react'
16
import React, { useCallback, useEffect, useMemo, useState } from 'react'
×
17
import { useTranslation } from 'react-i18next'
×
18
import { useSelector } from 'react-redux'
×
NEW
19
import { Link } from 'react-router'
×
20

21
import { useArticleActions } from '../hooks/article.js'
×
22
import useFetchData from '../hooks/graphql'
×
23
import { useModal } from '../hooks/modal.js'
×
24
import { useActiveWorkspaceId } from '../hooks/workspace.js'
×
25

26
import ArticleContributors from './ArticleContributors.jsx'
×
27
import ArticleSendCopy from './ArticleSendCopy.jsx'
×
28
import ArticleTags from './ArticleTags.jsx'
×
29
import ArticleVersionLinks from './ArticleVersionLinks.jsx'
×
30
import Button from './Button.jsx'
×
31
import CorpusSelectItems from './corpus/CorpusSelectItems.jsx'
×
32
import Export from './Export.jsx'
×
33
import Field from './Field.jsx'
×
34
import Modal from './Modal.jsx'
×
35
import FormActions from './molecules/FormActions.jsx'
×
36
import TimeAgo from './TimeAgo.jsx'
×
37
import WorkspaceSelectionItems from './workspace/WorkspaceSelectionItems.jsx'
×
38

39
import { getArticleContributors, getArticleTags } from './Article.graphql'
×
40
import { getTags } from './Tag.graphql'
×
41

42
import buttonStyles from './button.module.scss'
×
43
import fieldStyles from './field.module.scss'
×
44
import styles from './article.module.scss'
×
45

46
/**
47
 * @param props
48
 * @param {{title: string, owner: {displayName: string}, updatedAt: string, _id: string }} props.article
49
 * @param props.onArticleUpdated
50
 * @param props.onArticleDeleted
51
 * @param props.onArticleCreated
52
 * @return {Element}
53
 * @constructor
54
 */
55
export default function Article({
×
56
  article,
×
57
  onArticleUpdated,
×
58
  onArticleDeleted,
×
59
  onArticleCreated,
×
60
}) {
×
61
  const activeUser = useSelector((state) => state.activeUser)
×
62
  const articleId = useMemo(() => article._id, [article])
×
63
  const activeWorkspaceId = useActiveWorkspaceId()
×
64
  const articleActions = useArticleActions({ articleId })
×
65

66
  const { data: contributorsQueryData, error: contributorsError } =
×
67
    useFetchData(
×
68
      { query: getArticleContributors, variables: { articleId } },
×
69
      {
×
70
        fallbackData: {
×
71
          article,
×
72
        },
×
73
        revalidateIfStale: false,
×
74
        revalidateOnFocus: false,
×
75
        revalidateOnReconnect: false,
×
76
      }
×
77
    )
×
78
  const contributors = (
×
79
    contributorsQueryData?.article?.contributors || []
×
80
  ).filter((c) => c.user._id !== article.owner._id)
×
81
  const { data: userTagsQueryData } = useFetchData(
×
82
    { query: getTags, variables: {} },
×
83
    {
×
84
      revalidateIfStale: false,
×
85
      revalidateOnFocus: false,
×
86
      revalidateOnReconnect: false,
×
87
    }
×
88
  )
×
89
  const userTags = userTagsQueryData?.user?.tags || []
×
90
  const { data: articleTagsQueryData } = useFetchData(
×
91
    { query: getArticleTags, variables: { articleId } },
×
92
    {
×
93
      fallbackData: {
×
94
        article,
×
95
      },
×
96
      revalidateIfStale: false,
×
97
      revalidateOnFocus: false,
×
98
      revalidateOnReconnect: false,
×
99
    }
×
100
  )
×
101
  const tags = articleTagsQueryData?.article?.tags || []
×
102
  const { t } = useTranslation()
×
103
  const { setToast } = useToasts()
×
104

105
  const exportModal = useModal()
×
106
  const sharingModal = useModal()
×
107
  const sendCopyModal = useModal()
×
108
  const deleteModal = useModal()
×
109
  const [expanded, setExpanded] = useState(false)
×
110
  const [renaming, setRenaming] = useState(false)
×
111

112
  const [newTitle, setNewTitle] = useState(article.title)
×
113

114
  const isArticleOwner = activeUser._id === article.owner._id
×
115

116
  useEffect(() => {
×
117
    if (contributorsError) {
×
118
      setToast({
×
119
        type: 'error',
×
120
        text: `Unable to load contributors: ${contributorsError.toString()}`,
×
121
      })
×
122
    }
×
123
  }, [contributorsError])
×
124

125
  const toggleExpansion = useCallback(
×
126
    (event) => {
×
127
      if (!event.key || [' ', 'Enter'].includes(event.key)) {
×
128
        setExpanded(!expanded)
×
129
      }
×
130
    },
×
131
    [setExpanded, expanded]
×
132
  )
×
133

134
  const duplicate = async () => {
×
135
    const duplicatedArticleQuery = await articleActions.duplicate()
×
136
    onArticleCreated({
×
137
      ...article,
×
138
      ...duplicatedArticleQuery.duplicateArticle,
×
139
      contributors: [],
×
140
      versions: [],
×
141
    })
×
142
  }
×
143

144
  const rename = async (e) => {
×
145
    e.preventDefault()
×
146
    await articleActions.rename(newTitle)
×
147
    onArticleUpdated({
×
148
      ...article,
×
149
      title: newTitle,
×
150
    })
×
151
    setRenaming(false)
×
152
  }
×
153

154
  const handleDeleteArticle = async () => {
×
155
    try {
×
156
      await articleActions.remove()
×
157
      onArticleDeleted(article)
×
158
      setToast({
×
159
        type: 'default',
×
160
        text: t('article.delete.toastSuccess'),
×
161
      })
×
162
    } catch (err) {
×
163
      setToast({
×
164
        type: 'error',
×
165
        text: t('article.delete.toastError', { errMessage: err.message }),
×
166
      })
×
167
    }
×
168
  }
×
169

170
  const handleArticleTagsUpdated = useCallback(
×
171
    (event) => {
×
172
      onArticleUpdated({
×
173
        ...article,
×
174
        tags: event.updatedTags,
×
175
      })
×
176
    },
×
177
    [article]
×
178
  )
×
179

180
  return (
×
181
    <article className={styles.article}>
×
182
      <Modal
×
183
        {...exportModal.bindings}
×
184
        title={
×
185
          <>
×
186
            <Printer /> Export
×
187
          </>
×
188
        }
189
      >
190
        <Export
×
191
          articleId={article._id}
×
192
          bib={article.workingVersion?.bibPreview}
×
193
          name={article.title}
×
194
          onCancel={() => exportModal.close()}
×
195
        />
×
196
      </Modal>
×
197

198
      <Modal
×
199
        {...sharingModal.bindings}
×
200
        title={
×
201
          <>
×
202
            <UserPlus /> {t('article.shareModal.title')}
×
203
          </>
×
204
        }
205
        subtitle={t('article.shareModal.description')}
×
206
      >
207
        <ArticleContributors article={article} contributors={contributors} />
×
208
        <footer className={styles.actions}>
×
209
          <Button type="button" onClick={() => sharingModal.close()}>
×
210
            {t('modal.closeButton.text')}
×
211
          </Button>
×
212
        </footer>
×
213
      </Modal>
×
214

215
      <Modal
×
216
        {...sendCopyModal.bindings}
×
217
        title={
×
218
          <>
×
219
            <Send /> {t('article.sendCopyModal.title')}
×
220
          </>
×
221
        }
222
        subtitle={
×
223
          <>
×
224
            <span className={styles.sendText}>
×
225
              {t('article.sendCopyModal.description')}
×
226
            </span>
×
227
            <span>
×
228
              <Send className={styles.sendIcon} />
×
229
            </span>
×
230
          </>
×
231
        }
232
      >
233
        <ArticleSendCopy
×
234
          article={article}
×
235
          cancel={() => sendCopyModal.close()}
×
236
        />
×
237
        <footer className={styles.actions}>
×
238
          <Button type="button" onClick={() => sendCopyModal.close()}>
×
239
            {t('modal.closeButton.text')}
×
240
          </Button>
×
241
        </footer>
×
242
      </Modal>
×
243

244
      <Modal
×
245
        {...deleteModal.bindings}
×
246
        title={
×
247
          <>
×
248
            <Trash /> {t('article.deleteModal.title')}
×
249
          </>
×
250
        }
251
      >
252
        {t('article.deleteModal.confirmMessage')}
×
253
        {contributors && contributors.length > 0 && (
×
254
          <div className={clsx(styles.note, styles.important)}>
×
255
            {t('article.deleteModal.contributorsRemovalNote')}
×
256
          </div>
×
257
        )}
258
        <FormActions
×
259
          onSubmit={handleDeleteArticle}
×
260
          onCancel={() => deleteModal.close()}
×
261
          submitButton={{
×
262
            text: t('modal.deleteButton.text'),
×
263
            title: t('modal.deleteButton.text'),
×
264
          }}
×
265
        />
×
266
      </Modal>
×
267

268
      {!renaming && (
×
269
        <h1 className={styles.title} onClick={toggleExpansion}>
×
270
          <span tabIndex={0} onKeyUp={toggleExpansion} className={styles.icon}>
×
271
            {expanded ? <ChevronDown /> : <ChevronRight />}
×
272
          </span>
×
273

274
          <span>
×
275
            {article.title}
×
276
            <Button
×
277
              title={t('article.editName.button')}
×
278
              icon={true}
×
279
              className={styles.editTitleButton}
×
280
              onClick={(evt) => evt.stopPropagation() || setRenaming(true)}
×
281
            >
282
              <Edit3 size="20" />
×
283
            </Button>
×
284
          </span>
×
285
        </h1>
×
286
      )}
287
      {renaming && (
×
288
        <form
×
289
          className={clsx(styles.renamingForm, fieldStyles.inlineFields)}
×
290
          onSubmit={(e) => rename(e)}
×
291
        >
292
          <Field
×
293
            className={styles.inlineField}
×
294
            autoFocus={true}
×
295
            type="text"
×
296
            value={newTitle}
×
297
            onChange={(e) => setNewTitle(e.target.value)}
×
298
            placeholder="Article Title"
×
299
          />
×
300
          <Button
×
301
            title={t('article.editName.buttonSave')}
×
302
            primary={true}
×
303
            onClick={(e) => rename(e)}
×
304
          >
305
            <Check /> {t('article.editName.buttonSave')}
×
306
          </Button>
×
307
          <Button
×
308
            title={t('article.editName.buttonCancel')}
×
309
            type="button"
×
310
            onClick={() => {
×
311
              setRenaming(false)
×
312
              setNewTitle(article.title)
×
313
            }}
×
314
          >
315
            {t('article.editName.buttonCancel')}
×
316
          </Button>
×
317
        </form>
×
318
      )}
319

320
      <aside className={styles.actionButtons}>
×
321
        {isArticleOwner && !activeWorkspaceId && (
×
322
          <Button
×
323
            title={t('article.delete.button')}
×
324
            icon={true}
×
325
            onClick={() => deleteModal.show()}
×
326
          >
327
            <Trash />
×
328
          </Button>
×
329
        )}
330

331
        <Button
×
332
          title={t('article.duplicate.button')}
×
333
          icon={true}
×
334
          onClick={() => duplicate()}
×
335
        >
336
          <Copy />
×
337
        </Button>
×
338

339
        {
340
          <Button
×
341
            title={t('article.sendCopy.button')}
×
342
            icon={true}
×
343
            onClick={() => sendCopyModal.show()}
×
344
          >
345
            <Send />
×
346
          </Button>
×
347
        }
348

349
        {
350
          <Button
×
351
            title={t('article.share.button')}
×
352
            icon={true}
×
353
            onClick={() => sharingModal.show()}
×
354
          >
355
            <UserPlus />
×
356
          </Button>
×
357
        }
358

359
        <Button
×
360
          title={t('article.download.button')}
×
361
          icon={true}
×
362
          onClick={() => exportModal.show()}
×
363
        >
364
          <Printer />
×
365
        </Button>
×
366

367
        <Link
×
368
          title={t('article.editor.edit.title')}
×
369
          primary={true}
×
370
          className={buttonStyles.primary}
×
371
          to={`/article/${article._id}`}
×
372
        >
373
          <Pencil />
×
374
        </Link>
×
375

376
        <Link
×
377
          title={t('article.preview.button')}
×
378
          target="_blank"
×
379
          className={buttonStyles.icon}
×
380
          to={`/article/${article._id}/annotate`}
×
381
        >
382
          <Eye />
×
383
        </Link>
×
384
      </aside>
×
385

386
      <section className={styles.metadata}>
×
387
        <p className={styles.metadataAuthoring}>
×
388
          {tags.map((t) => (
×
389
            <span
×
390
              className={styles.tagChip}
×
391
              key={'tagColor-' + t._id}
×
392
              style={{ backgroundColor: t.color || 'grey' }}
×
393
            />
×
394
          ))}
×
395
          <span className={styles.by}>{t('article.by.text')}</span>{' '}
×
396
          <span className={styles.author}>{article.owner.displayName}</span>
×
397
          {contributors?.length > 0 && (
×
398
            <span className={styles.contributorNames}>
×
399
              <span>
×
400
                ,{' '}
×
401
                {contributors
×
402
                  .map((c) => c.user.displayName || c.user.username)
×
403
                  .join(', ')}
×
404
              </span>
×
405
            </span>
×
406
          )}
407
          <TimeAgo date={article.updatedAt} className={styles.momentsAgo} />
×
408
        </p>
×
409

410
        {expanded && (
×
411
          <div>
×
412
            <ArticleVersionLinks article={article} articleId={articleId} />
×
413

414
            {userTags.length > 0 && (
×
415
              <>
×
416
                <h4>{t('article.tags.title')}</h4>
×
417
                <div className={styles.editTags}>
×
418
                  <ArticleTags
×
419
                    articleId={article._id}
×
420
                    userTags={userTags}
×
421
                    onArticleTagsUpdated={handleArticleTagsUpdated}
×
422
                  />
×
423
                </div>
×
424
              </>
×
425
            )}
426

427
            <h4>{t('article.workspaces.title')}</h4>
×
428
            <ul className={styles.workspaces}>
×
429
              <WorkspaceSelectionItems articleId={articleId} />
×
430
            </ul>
×
431

432
            <h4>{t('article.corpus.title')}</h4>
×
433
            <ul className={styles.corpusList}>
×
434
              <CorpusSelectItems articleId={articleId} />
×
435
            </ul>
×
436
          </div>
×
437
        )}
438
      </section>
×
439
    </article>
×
440
  )
441
}
×
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

© 2025 Coveralls, Inc