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

inclusion-numerique / la-base / d2beb8a7-76db-4a5a-a1bb-93489e1772be

01 Apr 2026 09:30AM UTC coverage: 8.293% (-0.07%) from 8.36%
d2beb8a7-76db-4a5a-a1bb-93489e1772be

push

circleci

web-flow
Merge pull request #400 from inclusion-numerique/chore/rgaa-contreaudit

fix: contre audit rgaa

280 of 6384 branches covered (4.39%)

Branch coverage included in aggregate %.

0 of 122 new or added lines in 19 files covered. (0.0%)

8 existing lines in 7 files now uncovered.

1132 of 10643 relevant lines covered (10.64%)

0.59 hits per line

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

0.0
/apps/web/src/components/Resource/SaveResourceInCollectionModal.tsx
1
'use client'
2

3
import ExternalLink from '@app/ui/components/ExternalLink'
4
import InputFormField from '@app/ui/components/Form/InputFormField'
5
import { createDynamicModal } from '@app/ui/components/Modal/createDynamicModal'
6
import RawModal from '@app/ui/components/Modal/RawModal'
7
import { useModalVisibility } from '@app/ui/hooks/useModalVisibility'
8
import { createToast } from '@app/ui/toast/createToast'
9
import { sPluriel } from '@app/ui/utils/pluriel/sPluriel'
10
import type { SessionUser, SessionUserBase } from '@app/web/auth/sessionUser'
11
import BaseImage from '@app/web/components/BaseImage'
12
import EmptyBox from '@app/web/components/EmptyBox'
13
import RoundProfileImage from '@app/web/components/RoundProfileImage'
14
import { withTrpc } from '@app/web/components/trpc/withTrpc'
15
import VisibilityField from '@app/web/components/VisibilityField'
16
import { useIsMobile } from '@app/web/hooks/useIsMobile'
17
import { collectionTitleMaxLength } from '@app/web/server/collections/collectionConstraints'
18
import {
19
  type CreateCollectionCommand,
20
  CreateCollectionCommandValidation,
21
} from '@app/web/server/collections/createCollection'
22
import { trpc } from '@app/web/trpc'
23
import { applyZodValidationMutationErrorsToForm } from '@app/web/utils/applyZodValidationMutationErrorsToForm'
24
import Button from '@codegouvfr/react-dsfr/Button'
25
import Notice from '@codegouvfr/react-dsfr/Notice'
26
import { zodResolver } from '@hookform/resolvers/zod'
27
import * as Sentry from '@sentry/nextjs'
28
import classNames from 'classnames'
29
import { useRouter } from 'next/navigation'
30
import { type FormEvent, useEffect, useState } from 'react'
31
import { useForm } from 'react-hook-form'
32
import AddOrRemoveResourceFromCollection from './AddOrRemoveResourceFromCollection'
33
import SaveInNestedCollection from './SaveInNestedCollection'
34
import styles from './SaveResourceInCollectionModal.module.css'
35

36
const titleInfo = (title?: string | null) =>
×
37
  `${title?.length ?? 0}/${collectionTitleMaxLength} caractères`
×
38

39
export const SaveResourceInCollectionDynamicModal = createDynamicModal({
×
40
  id: 'save-resource-in-collection',
41
  isOpenedByDefault: false,
42
  initialState: {
43
    resourceId: null as string | null,
44
    shareableLinkId: null as string | null,
45
  },
46
})
47

48
/**
49
 * This modal is used to save a resource in a collection
50
 * and to quickly create a collection.
51
 *
52
 * The title and buttons of the modal must be top level, component is too complex
53
 * and could be broken down in multiple hooks and components for the different "steps" of the modal.
54
 */
55
const SaveResourceInCollectionModal = ({ user }: { user: SessionUser }) => {
×
56
  const { resourceId, shareableLinkId } =
57
    SaveResourceInCollectionDynamicModal.useState()
×
58
  const router = useRouter()
×
59

60
  const bases = user.bases.map(({ base }) => base)
×
61

62
  // User can create a collection in a base or in his profile from this modal
63
  const createCollectionMutation = trpc.collection.create.useMutation()
×
64

65
  const isUserPublic = user.isPublic
×
66

67
  const createCollectionForm = useForm<CreateCollectionCommand>({
×
68
    resolver: zodResolver(CreateCollectionCommandValidation),
69
    defaultValues: {
70
      // Let the user chose the visibility only if possible
71
      isPublic: isUserPublic ? undefined : false,
×
72
    },
73
  })
74

75
  const createCollectionFormReset = createCollectionForm.reset
×
76

77
  // If the user has no base, collections are limited to the profile
78
  const profileOnly = bases.length === 0
×
79

80
  // If the user has bases, he can see collections from the profiles (or bases)
81
  const [inProfileDirectory, setInProfileDirectory] = useState(false)
×
82

83
  const viewProfileDirectory = () => {
×
84
    setInProfileDirectory(true)
×
85
    createCollectionForm.reset({
×
86
      addResourceId: resourceId,
87
      isPublic: user.isPublic ? undefined : false,
×
88
      baseId: null,
89
      title: '',
90
    })
91
  }
92

93
  // The base if navigating in a base directory
94
  const [inBaseDirectory, setInBaseDirectory] =
95
    useState<SessionUserBase | null>(null)
×
96

97
  const viewBaseDirectory = (baseId: string) => {
×
98
    const selectedBase = bases.find((base) => base.id === baseId)
×
99
    if (!selectedBase) {
×
100
      return
×
101
    }
102
    setInBaseDirectory(selectedBase)
×
103
    createCollectionForm.reset({
×
104
      addResourceId: resourceId,
105
      isPublic: selectedBase.isPublic ? undefined : false,
×
106
      baseId: selectedBase.id,
107
      title: '',
108
    })
109
  }
110

111
  const goBackFromDirectory = () => {
×
112
    setInBaseDirectory(null)
×
113
    setInProfileDirectory(false)
×
114
    createCollectionForm.reset({
×
115
      addResourceId: resourceId,
116
      isPublic: user.isPublic ? undefined : false,
×
117
      baseId: undefined,
118
      title: '',
119
    })
120
  }
121

122
  const [inCollectionCreation, setInCollectionCreation] = useState(false)
×
123

124
  const viewCollectionCreation = (baseId?: string) => {
×
125
    createCollectionForm.reset({
×
126
      addResourceId: resourceId,
127
      isPublic: user.isPublic ? undefined : false,
×
128
      baseId,
129
      title: '',
130
    })
131
    const selectedBase = bases.find((base) => base.id === baseId)
×
132
    if (selectedBase) {
×
133
      createCollectionForm.reset({
×
134
        addResourceId: resourceId,
135
        isPublic: selectedBase.isPublic ? undefined : false,
×
136
        baseId: selectedBase.id,
137
        title: '',
138
      })
139
    }
140

141
    setInCollectionCreation(true)
×
142
  }
143

144
  const cancelCollectionCreation = () => {
×
145
    setInCollectionCreation(false)
×
146
  }
147
  /**
148
   * Reset modal state when the resource changes
149
   */
150
  useEffect(() => {
×
151
    createCollectionFormReset({
×
152
      addResourceId: resourceId,
153
      isPublic: isUserPublic ? undefined : false,
×
154
      baseId: null,
155
      title: '',
156
    })
157
    setInBaseDirectory(null)
×
158
    setInProfileDirectory(false)
×
159
    setInCollectionCreation(false)
×
160
  }, [resourceId, createCollectionFormReset, isUserPublic])
161

162
  /**
163
   * Reset modal state on modal close
164
   */
165
  useModalVisibility(SaveResourceInCollectionDynamicModal.id, {
×
166
    onClosed: () => {
167
      createCollectionFormReset({
×
168
        addResourceId: resourceId,
169
        isPublic: isUserPublic ? undefined : false,
×
170
        baseId: null,
171
        title: '',
172
      })
173
      setInBaseDirectory(null)
×
174
      setInProfileDirectory(false)
×
175
      setInCollectionCreation(false)
×
176
    },
177
  })
178

179
  // Used to display which collection is in "loading" state when adding or removing to/from a collection
180
  const [pendingMutationCollectionId, setPendingMutationCollectionId] =
181
    useState<string | null>(null)
×
182

183
  const addToCollectionMutation = trpc.resource.addToCollection.useMutation()
×
184
  const removeFromCollectionMutation =
185
    trpc.resource.removeFromCollection.useMutation()
×
186

187
  const onAddToCollection = async (collectionId: string) => {
×
188
    if (!resourceId) {
×
189
      return
×
190
    }
191
    setPendingMutationCollectionId(collectionId)
×
192
    try {
×
193
      const result = await addToCollectionMutation.mutateAsync({
×
194
        resourceId,
195
        collectionId,
196
        shareableLinkId: shareableLinkId ?? undefined,
×
197
      })
198
      setPendingMutationCollectionId(null)
×
199
      router.refresh()
×
200
      createToast({
×
201
        priority: 'success',
202
        message: (
203
          <>
204
            Ajoutée à la collection&nbsp;
205
            <strong className={styles.title}>{result.collection.title}</strong>
206
          </>
207
        ),
208
      })
209
      SaveResourceInCollectionDynamicModal.close()
×
210
    } catch (error) {
211
      createToast({
×
212
        priority: 'error',
213
        message: 'Une erreur est survenue lors de l’ajout à la collection',
214
      })
215
      Sentry.captureException(error)
×
216
      throw error
×
217
    }
218
  }
219

220
  const onRemoveFromCollection = async (collectionId: string) => {
×
221
    if (!resourceId) {
×
222
      return
×
223
    }
224
    setPendingMutationCollectionId(collectionId)
×
225
    try {
×
226
      const result = await removeFromCollectionMutation.mutateAsync({
×
227
        resourceId,
228
        collectionId,
229
      })
230
      setPendingMutationCollectionId(null)
×
231
      router.refresh()
×
232
      createToast({
×
233
        priority: 'success',
234
        message: (
235
          <>
236
            Retirée de la collection <strong>{result.collection.title}</strong>
237
          </>
238
        ),
239
      })
240

241
      SaveResourceInCollectionDynamicModal.close()
×
242
    } catch (error) {
243
      createToast({
×
244
        priority: 'error',
245
        message: 'Une erreur est survenue lors du retrait de la collection',
246
      })
247
      Sentry.captureException(error)
×
248
      throw error
×
249
    }
250
  }
251

252
  const onCreateCollection = async (data: CreateCollectionCommand) => {
×
253
    try {
×
254
      const collection = await createCollectionMutation.mutateAsync({
×
255
        ...data,
256
        addResourceShareableLinkId: shareableLinkId ?? undefined,
×
257
      })
258

259
      createToast({
×
260
        priority: 'success',
261
        message: (
262
          <>
263
            Enregistrée dans <strong>{collection.title}</strong>
264
          </>
265
        ),
266
      })
267

268
      router.refresh()
×
269
      SaveResourceInCollectionDynamicModal.close()
×
270

271
      // There is a bug here where useModalVisibility onClose do not trigger :(
272
      // Here is a setTimeout hack to fix it
273
      setTimeout(() => {
×
274
        createCollectionFormReset({
×
275
          addResourceId: resourceId,
276
          isPublic: isUserPublic ? undefined : false,
×
277
          baseId: null,
278
          title: '',
279
        })
280
        setInBaseDirectory(null)
×
281
        setInProfileDirectory(false)
×
282
        setInCollectionCreation(false)
×
283
      }, 800)
284
    } catch (error) {
285
      if (
×
286
        applyZodValidationMutationErrorsToForm(
287
          error,
288
          createCollectionForm.setError,
289
        )
290
      ) {
291
        return
×
292
      }
293

294
      createToast({
×
295
        priority: 'error',
296
        message: 'Une erreur est survenue lors de la création de la collection',
297
      })
298
      Sentry.captureException(error)
×
299

300
      throw error
×
301
    }
302
  }
303
  const withoutFavoriteCollections = user.collections.filter(
×
304
    (c) => !c.isFavorites,
×
305
  )
306
  const showCreateCollectionButton =
307
    !inCollectionCreation &&
×
308
    ((profileOnly && withoutFavoriteCollections.length > 0) ||
309
      (!!inBaseDirectory && inBaseDirectory.collections.length > 0) ||
310
      inProfileDirectory)
311

312
  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
×
313
    event.stopPropagation()
×
314
    event.preventDefault()
×
315
    if (inCollectionCreation) {
×
316
      createCollectionForm
×
317
        .handleSubmit(onCreateCollection)()
318
        .catch(() => {
319
          // Error is caught in the onSubmit
320
        })
321
    }
322
  }
323

324
  const collectionCannotBePublic = inBaseDirectory
×
325
    ? !inBaseDirectory.isPublic
326
    : !user.isPublic
327

328
  const creationLoading =
329
    createCollectionForm.formState.isSubmitting ||
×
330
    createCollectionForm.formState.isSubmitSuccessful
331

332
  const favoriteCollection = user.collections.find((c) => c.isFavorites)
×
333
  const isMobile = useIsMobile()
×
334
  const avatarSize = isMobile ? 32 : 48
×
NEW
335
  const expandedBaseId = inBaseDirectory?.id
×
UNCOV
336
  return (
×
337
    <form onSubmit={onSubmit}>
338
      <RawModal
339
        className={styles.modal}
340
        title={
341
          inCollectionCreation ? 'Créer une collection' : 'Enregistrer dans :'
×
342
        }
343
        id={SaveResourceInCollectionDynamicModal.id}
344
        buttons={
345
          inCollectionCreation
×
346
            ? [
347
                {
348
                  children: 'Créer la collection',
349
                  priority: 'primary',
350
                  type: 'submit',
351
                  doClosesModal: false,
352
                  className: creationLoading ? 'fr-btn--loading' : undefined,
×
353
                  nativeButtonProps: {
354
                    key: 'save-collection',
355
                  },
356
                },
357
                {
358
                  children: 'Précédent',
359
                  priority: 'secondary',
360
                  type: 'button',
361
                  onClick: cancelCollectionCreation,
362
                  doClosesModal: false,
363
                  nativeButtonProps: {
364
                    key: 'cancel-collection',
365
                  },
366
                },
367
              ]
368
            : showCreateCollectionButton
×
369
              ? [
370
                  {
371
                    children: 'Créer une collection',
372
                    type: 'button',
373
                    priority: 'secondary',
374
                    iconId: 'ri-folder-add-line',
375
                    doClosesModal: false,
376
                    className: styles.createCollectionButton,
377
                    onClick: () => viewCollectionCreation(inBaseDirectory?.id),
×
378
                    nativeButtonProps: {
379
                      key: 'view-collection-creation',
380
                    },
381
                  },
382
                ]
383
              : undefined
384
        }
385
      >
386
        {/* Navigation if in directory */}
387
        {!inCollectionCreation && (inProfileDirectory || !!inBaseDirectory) && (
×
388
          <button
389
            type="button"
390
            className={classNames(
391
              styles.clickableContainer,
392
              styles.backToBasesButton,
393
              'fr-border--bottom',
394
            )}
395
            onClick={goBackFromDirectory}
396
            data-testid="back-to-bases-button"
397
          >
398
            <div className="fr-flex fr-flex-gap-4v fr-align-items-center">
399
              <span
400
                className={classNames(
401
                  'fr-icon-arrow-left-s-line',
402
                  'fr-icon--sm',
403
                  'fr-mx-1w',
404
                  styles.arrow,
405
                )}
406
              />
407
              {inBaseDirectory ? (
×
408
                <BaseImage base={inBaseDirectory} size={avatarSize} />
409
              ) : (
410
                <RoundProfileImage
411
                  user={user}
412
                  borderWidth={1}
413
                  size={avatarSize}
414
                />
415
              )}
416
              <div className="fr-flex fr-direction-column fr-flex-gap-1v">
417
                <h2
418
                  className={classNames(
419
                    'fr-text--md fr-text--bold fr-mb-0 fr-text-title--grey fr-text--start',
420
                    styles.title,
421
                  )}
422
                >
423
                  {inBaseDirectory
×
424
                    ? inBaseDirectory.title
425
                    : `${user.name} - Mes collections`}
426
                </h2>
427
                <p className={classNames('fr-mb-0', styles.collections)}>
428
                  <span className="fr-icon-folder-2-line fr-icon--sm" />
429
                  &nbsp;
430
                  {inBaseDirectory
×
431
                    ? inBaseDirectory.collections.length
432
                    : withoutFavoriteCollections.length}
433
                  &nbsp;Collection
434
                  {sPluriel(
435
                    inBaseDirectory?.collections.length ??
×
436
                      withoutFavoriteCollections.length,
437
                  )}
438
                </p>
439
              </div>
440
            </div>
441
          </button>
442
        )}
443
        {/* Add/remove mutation error */}
444
        {!inCollectionCreation &&
×
445
          (addToCollectionMutation.error ||
446
            removeFromCollectionMutation.error) && (
447
            <p
448
              className="fr-error-text"
449
              data-testid="save-resource-in-collection-error"
450
            >
451
              {addToCollectionMutation.error?.message ??
×
452
                removeFromCollectionMutation.error?.message}
453
            </p>
454
          )}
455
        {!!resourceId &&
×
456
          (inCollectionCreation ? (
×
457
            <>
458
              <p className="fr-text-mention--grey fr-text--xs">
459
                Les champs avec une astérisque sont obligatoires. Vous pourrez
460
                modifier ces informations plus tard.
461
              </p>
462

463
              <InputFormField
464
                data-testid="collection-title-input"
465
                control={createCollectionForm.control}
466
                path="title"
467
                label={
468
                  <>
469
                    Nom de la collection{' '}
470
                    <span className="fr-sr-only">
471
                      {collectionTitleMaxLength} caractères maximums autorisés
472
                    </span>
473
                  </>
474
                }
475
                disabled={createCollectionForm.formState.isSubmitting}
476
                asterisk
477
                info={titleInfo}
478
              />
479
              {/* Display info if cannot be public */}
480
              {collectionCannotBePublic ? (
×
481
                <Notice
482
                  title={
483
                    inBaseDirectory ? (
×
484
                      <span className="fr-flex fr-direction-column fr-flex-gap-1v">
485
                        <span className="fr-text-mention--black fr-text--bold">
486
                          Base privée
487
                        </span>
488
                        <span className="fr-text-mention--grey fr-text--regular">
489
                          La collection sera accessible uniquement aux membres
490
                          et aux administrateurs de votre base.
491
                        </span>
492
                      </span>
493
                    ) : (
494
                      <span className="fr-flex fr-direction-column fr-flex-gap-1v">
495
                        <span className="fr-text-mention--black fr-text--bold">
496
                          Profil privé
497
                        </span>
498
                        <span className="fr-text-mention--grey fr-text--regular">
499
                          Votre collection sera visible uniquement par vous.
500
                        </span>
501
                      </span>
502
                    )
503
                  }
504
                  classes={{
505
                    title: styles.noticeContainer,
506
                  }}
507
                />
508
              ) : (
509
                <VisibilityField
510
                  model="Collection"
511
                  path="isPublic"
512
                  control={createCollectionForm.control}
513
                  disabled={createCollectionForm.formState.isSubmitting}
514
                  privateTitle="Collection privée"
515
                  publicTitle="Collection publique"
516
                  publicHint="Visible par tous les visiteurs."
517
                  privateHint="Accessible uniquement aux membres et aux administrateurs que vous inviterez."
518
                  label="Visibilité de la collection"
519
                  asterisk
520
                />
521
              )}
522
            </>
523
          ) : inProfileDirectory || profileOnly ? (
×
524
            <>
525
              {!!favoriteCollection && profileOnly && (
×
526
                <div
527
                  className={classNames(
528
                    withoutFavoriteCollections.length > 0 &&
×
529
                      'fr-border--bottom',
530
                  )}
531
                >
532
                  <AddOrRemoveResourceFromCollection
533
                    loading={
534
                      pendingMutationCollectionId === favoriteCollection.id
535
                    }
536
                    key={favoriteCollection.id}
537
                    collection={favoriteCollection}
538
                    resourceId={resourceId}
539
                    onAdd={onAddToCollection}
540
                    onRemove={onRemoveFromCollection}
541
                    withPrivacyTag
542
                  />
543
                </div>
544
              )}
545
              {withoutFavoriteCollections.length > 0 ? (
×
546
                <>
547
                  <h2 className="fr-mt-4v fr-mb-0 fr-text--xs fr-text--bold fr-text-mention--grey fr-text--uppercase">
548
                    Mes collections
549
                  </h2>
550
                  <ul className="fr-raw-list">
551
                    {withoutFavoriteCollections.map((collection) => (
552
                      <li key={collection.id}>
×
553
                        <AddOrRemoveResourceFromCollection
554
                          loading={
555
                            pendingMutationCollectionId === collection.id
556
                          }
557
                          collection={collection}
558
                          resourceId={resourceId}
559
                          onAdd={onAddToCollection}
560
                          onRemove={onRemoveFromCollection}
561
                          withPrivacyTag={!collection.isPublic}
562
                        />
563
                      </li>
564
                    ))}
565
                  </ul>
566
                </>
567
              ) : (
568
                <EmptyBox
569
                  title="Vous n’avez pas de collection dans votre profil."
570
                  className="fr-mt-6v fr-p-6v fr-p-md-8v fr-py-md-0"
571
                  titleAs="h5"
572
                >
573
                  <p>
574
                    Créez une collection pour enregistrer, organiser, partager
575
                    facilement des ressources.&nbsp;
576
                    <ExternalLink
577
                      href="https://docs.numerique.gouv.fr/docs/5f8d928b-2fd7-4f4a-b8fd-ca9c841dc841/"
578
                      className="fr-link"
579
                    >
580
                      En savoir plus
581
                    </ExternalLink>
582
                  </p>
583
                  <div data-testid="create-resource-button">
584
                    <Button
585
                      type="button"
586
                      onClick={() =>
587
                        viewCollectionCreation(inBaseDirectory?.id)
×
588
                      }
589
                      nativeButtonProps={{
590
                        key: 'view-collection-creation',
591
                      }}
592
                    >
593
                      <span className="ri-folder-add-line fr-mr-1w" />
594
                      Créer une collection
595
                    </Button>
596
                  </div>
597
                </EmptyBox>
598
              )}
599
            </>
600
          ) : inBaseDirectory ? (
×
601
            inBaseDirectory.collections.length > 0 ? (
×
602
              <ul className="fr-raw-list">
603
                {inBaseDirectory.collections.map((collection) => (
604
                  <li key={collection.id}>
×
605
                    <AddOrRemoveResourceFromCollection
606
                      loading={pendingMutationCollectionId === collection.id}
607
                      collection={collection}
608
                      resourceId={resourceId}
609
                      onAdd={onAddToCollection}
610
                      onRemove={onRemoveFromCollection}
611
                      withPrivacyTag={!collection.isPublic}
612
                    />
613
                  </li>
614
                ))}
615
              </ul>
616
            ) : (
617
              <EmptyBox
618
                title="Vous n’avez pas de collection dans votre base."
619
                className="fr-mt-6v fr-p-md-8v fr-py-md-0"
620
                titleAs="h5"
621
                data-testid="base-without-collection"
622
              >
623
                <p>
624
                  Créez une collection pour enregistrer, organiser, partager
625
                  facilement des ressources.&nbsp;
626
                  <ExternalLink
627
                    href="https://docs.numerique.gouv.fr/docs/5f8d928b-2fd7-4f4a-b8fd-ca9c841dc841/"
628
                    className="fr-link"
629
                  >
630
                    En savoir plus
631
                  </ExternalLink>
632
                </p>
633
                <div data-testid="create-resource-button">
634
                  <Button
635
                    type="button"
636
                    onClick={() => viewCollectionCreation(inBaseDirectory?.id)}
×
637
                    nativeButtonProps={{
638
                      key: 'view-collection-creation',
639
                    }}
640
                  >
641
                    <span className="ri-folder-add-line fr-mr-1w" />
642
                    Créer une collection
643
                  </Button>
644
                </div>
645
              </EmptyBox>
646
            )
647
          ) : (
648
            <ul className="fr-raw-list">
649
              {!!favoriteCollection && (
×
650
                <li
651
                  className={classNames(
652
                    (withoutFavoriteCollections.length > 0 ||
×
653
                      bases.length > 0) &&
654
                      'fr-border--bottom',
655
                  )}
656
                >
657
                  <AddOrRemoveResourceFromCollection
658
                    loading={
659
                      pendingMutationCollectionId === favoriteCollection.id
660
                    }
661
                    collection={favoriteCollection}
662
                    resourceId={resourceId}
663
                    onAdd={onAddToCollection}
664
                    onRemove={onRemoveFromCollection}
665
                    withPrivacyTag
666
                  />
667
                </li>
668
              )}
669
              {withoutFavoriteCollections.length > 0 && (
×
670
                <li>
671
                  <SaveInNestedCollection
672
                    user={user}
673
                    onClick={viewProfileDirectory}
674
                    alreadyInCollections={
675
                      withoutFavoriteCollections.filter(({ resources }) =>
676
                        resources.some(
×
677
                          (collectionResource) =>
678
                            collectionResource.resourceId === resourceId,
×
679
                        ),
680
                      ).length
681
                    }
682
                    isExpanded={inProfileDirectory}
683
                  />
684
                </li>
685
              )}
686
              {bases.map((base) => (
687
                <li key={base.id}>
×
688
                  <SaveInNestedCollection
689
                    user={user}
690
                    base={base}
691
                    onClick={() => {
692
                      viewBaseDirectory(base.id)
×
693
                    }}
694
                    alreadyInCollections={
695
                      base.collections.filter(({ resources }) =>
696
                        resources.some(
×
697
                          (collectionResource) =>
698
                            collectionResource.resourceId === resourceId,
×
699
                        ),
700
                      ).length
701
                    }
702
                    isExpanded={expandedBaseId === base.id}
703
                  />
704
                </li>
705
              ))}
706
            </ul>
707
          ))}
708
      </RawModal>
709
    </form>
710
  )
711
}
712

713
export default withTrpc(SaveResourceInCollectionModal)
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