diff --git a/index.html b/index.html
index fec06e0d..74516e4d 100644
--- a/index.html
+++ b/index.html
@@ -21,7 +21,7 @@
-
+
Polyflix
diff --git a/public/locales/en/attachments.json b/public/locales/en/attachments.json
index c0f66d05..b5345a5f 100644
--- a/public/locales/en/attachments.json
+++ b/public/locales/en/attachments.json
@@ -30,6 +30,10 @@
}
},
"errors": {}
+ },
+ "selector": {
+ "title": "Select attachments",
+ "validate": "Close"
}
},
"closeModal": "Close",
diff --git a/public/locales/en/videos.json b/public/locales/en/videos.json
index fa4701c7..12e27b13 100644
--- a/public/locales/en/videos.json
+++ b/public/locales/en/videos.json
@@ -51,16 +51,18 @@
"youtubeUrl": "YouTube video URL",
"description": "Description",
"upload": "Drag 'n' drop your video here.",
- "attachments": {
- "label": "Label",
- "url": "Attachment URL",
- "empty": "Your video doesn't have attachments."
- },
"submit": {
"create": "Create video",
"update": "Update video"
}
},
+ "attachments": {
+ "label": "Attachments",
+ "description": "You can add or remove attachments to your video.",
+ "add": "Add attachments",
+ "remove": "Remove attachment",
+ "empty": "Your video does not include any attachments yet."
+ },
"errors": {
"upload": "An error occured when uploading your file. Please see the logs for more informations."
},
diff --git a/public/locales/fr/attachments.json b/public/locales/fr/attachments.json
index 00fc7667..9de64865 100644
--- a/public/locales/fr/attachments.json
+++ b/public/locales/fr/attachments.json
@@ -30,6 +30,10 @@
}
},
"errors": {}
+ },
+ "selector": {
+ "title": "Veuillez sélectionner les pièces jointes",
+ "validate": "Fermer"
}
},
"closeModal": "Fermer",
@@ -44,4 +48,4 @@
"actions": {
"copyToClipboard": "Copier le lien dans le presse-papier"
}
-}
\ No newline at end of file
+}
diff --git a/public/locales/fr/videos.json b/public/locales/fr/videos.json
index 07c2fb32..d340a989 100644
--- a/public/locales/fr/videos.json
+++ b/public/locales/fr/videos.json
@@ -56,16 +56,18 @@
"youtubeUrl": "Lien de la vidéo sur YouTube",
"description": "Description",
"upload": "Glissez déposer votre vidéo ici",
- "attachments": {
- "label": "Nom",
- "url": "Lien de la pièce jointe",
- "empty": "Votre vidéo n'a aucune pièces jointes."
- },
"submit": {
"create": "Créer la vidéo",
"update": "Mettre à jour la vidéo"
}
},
+ "attachments": {
+ "label": "Pièces jointes",
+ "description": "Vous pouvez ajouter ou supprimer des pièces jointes à votre vidéo.",
+ "add": "Ajouter des pièces jointes",
+ "remove": "Supprimer la pièce jointe",
+ "empty": "Votre vidéo n'a aucune pièces jointes."
+ },
"errors": {
"upload": "Une erreur est survenue lors de l'envoi de vos fichiers."
},
diff --git a/src/modules/attachments/components/AttachmentAvatar.component.tsx b/src/modules/attachments/components/AttachmentAvatar.component.tsx
new file mode 100644
index 00000000..b90080dd
--- /dev/null
+++ b/src/modules/attachments/components/AttachmentAvatar.component.tsx
@@ -0,0 +1,49 @@
+import { Avatar, IconButton, ListItemIcon, Tooltip } from '@mui/material'
+import CopyToClipboard from 'react-copy-to-clipboard'
+import { useTranslation } from 'react-i18next'
+
+import { useInjection } from '@polyflix/di'
+
+import { Icon } from '@core/components/Icon/Icon.component'
+import { SnackbarService } from '@core/services/snackbar.service'
+
+type Props = {
+ url: string
+ copyToClipboard?: boolean
+}
+
+export const AttachmentAvatar = ({ url, copyToClipboard = true }: Props) => {
+ const { t: tUsers } = useTranslation('users')
+ const { t: tAttachments } = useTranslation('attachments')
+ const snackbarService = useInjection(SnackbarService)
+
+ const avatarContent = () => (
+
+
+
+ )
+
+ if (copyToClipboard) {
+ return (
+
+ {
+ snackbarService.createSnackbar(
+ tUsers('profile.tabs.attachments.content.list.clipboard'),
+ {
+ variant: 'success',
+ }
+ )
+ }}
+ text={url}
+ >
+ ('actions.copyToClipboard')}>
+ {avatarContent()}
+
+
+
+ )
+ } else {
+ return {avatarContent()}
+ }
+}
diff --git a/src/modules/attachments/components/AttachmentSelectorModal.component.tsx b/src/modules/attachments/components/AttachmentSelectorModal.component.tsx
new file mode 100644
index 00000000..a5671f64
--- /dev/null
+++ b/src/modules/attachments/components/AttachmentSelectorModal.component.tsx
@@ -0,0 +1,201 @@
+import {
+ Box,
+ Button,
+ Checkbox,
+ Container,
+ Fade,
+ Link,
+ ListItem,
+ ListItemButton,
+ ListItemText,
+ Modal,
+ Paper,
+ Stack,
+ SxProps,
+ Theme,
+ Typography,
+} from '@mui/material'
+import { useEffect, useState } from 'react'
+import { UseFieldArrayReturn } from 'react-hook-form'
+import { useTranslation } from 'react-i18next'
+import { Redirect } from 'react-router-dom'
+
+import { NoData } from '@core/components/NoData/NoData.component'
+import { PaginationSynced } from '@core/components/Pagination/PaginationSynced.component'
+import { Scrollbar } from '@core/components/Scrollbar/Scrollbar.component'
+import { buildSkeletons } from '@core/utils/gui.utils'
+
+import { useAuth } from '@auth/hooks/useAuth.hook'
+
+import { IVideoForm } from '@videos/types/form.type'
+
+import { Attachment } from '@attachments/models/attachment.model'
+import { AttachmentParams } from '@attachments/models/attachment.params'
+import { useGetUserAttachmentsQuery } from '@attachments/services/attachment.service'
+
+import { AttachmentAvatar } from './AttachmentAvatar.component'
+
+interface Props {
+ attachments: UseFieldArrayReturn
+ videoId?: string
+ isOpen: boolean
+ onClose: () => void
+ sx?: SxProps
+}
+export const AttachmentSelectorModal = ({
+ attachments,
+ videoId,
+ isOpen,
+ onClose,
+ sx: sxProps,
+}: Props) => {
+ const { user } = useAuth()
+ const { t } = useTranslation('attachments')
+
+ const [page] = useState(1)
+
+ const [filters, setFilters] = useState({
+ page,
+ pageSize: 10,
+ userId: user!.id,
+ })
+
+ const { data, isLoading } = useGetUserAttachmentsQuery(filters)
+
+ const { fields, append, remove } = attachments
+
+ const handleToggle = (attachment: Attachment) => () => {
+ const currentIndex = fields.findIndex((e) => e.id === attachment.id)
+ if (currentIndex === -1) {
+ append(attachment)
+ } else {
+ remove(currentIndex)
+ }
+ }
+
+ useEffect(() => {
+ if (videoId && data) {
+ for (const a of data.items) {
+ if (a.videos.includes(videoId)) {
+ append(a)
+ }
+ }
+ }
+ }, [videoId, data])
+
+ const isAttachmentSelected = (attachment: Attachment) =>
+ fields.some((e) => e.id === attachment.id)
+
+ /* If the user has no attachment, he is redirected to the attachment creation form */
+ if (data && data.totalCount === 0)
+ return
+
+ return (
+ onClose()}
+ aria-labelledby="element modal"
+ closeAfterTransition
+ BackdropProps={{
+ timeout: 500,
+ }}
+ >
+
+
+
+ {t('forms.selector.title')}
+
+ `calc(100vh - ${theme.spacing(30)})`,
+ minHeight: '300px',
+ }}
+ >
+
+ {{ data }
+ ? data?.items.map((item) => (
+
+ }
+ disablePadding
+ >
+
+
+
+
+
+
+
+ ))
+ : buildSkeletons(3)}
+
+
+
+ {!isLoading &&
+ (data &&
+ data.items.length > 0 &&
+ data.items.length < data.totalCount ? (
+
+
+
+ ) : (
+ !data ||
+ (data.items.length === 0 && (
+
+ ))
+ ))}
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/modules/attachments/services/attachment.service.ts b/src/modules/attachments/services/attachment.service.ts
index 6158d9f3..7c6ffe07 100644
--- a/src/modules/attachments/services/attachment.service.ts
+++ b/src/modules/attachments/services/attachment.service.ts
@@ -40,7 +40,7 @@ export const attachmentsApi = createApi({
}),
getVideoAttachments: builder.query({
query: (videoId: string) => {
- return `${Endpoint.Attachments}/video/${videoId}`
+ return `${Endpoint.Attachments}/video/${videoId}?pageSize=50&page=1` // TODO : remove pagination from attachment service (only for /video/id)
},
providesTags: (result) =>
result
diff --git a/src/modules/users/pages/ProfileAttachments/ProfileAttachmentListItem.component.tsx b/src/modules/users/pages/ProfileAttachments/ProfileAttachmentListItem.component.tsx
index 894f0773..adc4ce59 100644
--- a/src/modules/users/pages/ProfileAttachments/ProfileAttachmentListItem.component.tsx
+++ b/src/modules/users/pages/ProfileAttachments/ProfileAttachmentListItem.component.tsx
@@ -7,16 +7,11 @@ import {
ListItemText,
Paper,
Skeleton,
- Tooltip,
} from '@mui/material'
-import CopyToClipboard from 'react-copy-to-clipboard'
-import { useTranslation } from 'react-i18next'
-
-import { useInjection } from '@polyflix/di'
import { Icon } from '@core/components/Icon/Icon.component'
-import { SnackbarService } from '@core/services/snackbar.service'
+import { AttachmentAvatar } from '@attachments/components/AttachmentAvatar.component'
import { Attachment } from '@attachments/models/attachment.model'
import { AttachmentListMenu } from '@users/components/AttachmentListMenu/AttachmentListMenu.component'
@@ -26,10 +21,6 @@ type Props = {
onDelete: () => void
}
export const ProfileAttachmentListItem = ({ attachment, onDelete }: Props) => {
- const { t: tUsers } = useTranslation('users')
- const { t: tAttachments } = useTranslation('attachments')
- const snackbarService = useInjection(SnackbarService)
-
return (
{
variant="outlined"
sx={{ mb: 1 }}
>
-
- {
- snackbarService.createSnackbar(
- tUsers('profile.tabs.attachments.content.list.clipboard'),
- {
- variant: 'success',
- }
- )
- }}
- text={attachment.url}
- >
- ('actions.copyToClipboard')}>
-
-
-
-
-
-
-
-
+
{
const snackbarService = useInjection(SnackbarService)
const minioService = useInjection(MinioService)
const [isInProgress, setIsInProgress] = useState(false)
+ const [isModalAttachmentOpen, setIsModalAttachmentOpen] = useState(false)
const { t } = useTranslation('videos')
@@ -75,6 +95,7 @@ export const VideoForm = ({ source, video, isUpdate }: Props) => {
}
const {
+ control,
register,
handleSubmit,
formState: { errors, isSubmitting },
@@ -91,10 +112,14 @@ export const VideoForm = ({ source, video, isUpdate }: Props) => {
visibility: video?.visibility || Visibility.PUBLIC,
thumbnail: video?.thumbnail,
source: video?.source.replace('-nocookie', ''),
- attachments: video?.attachments,
+ attachments: video
+ ? useGetVideoAttachmentsQuery(video.id).data?.items || []
+ : [],
},
})
+ const attachments = useFieldArray({ control, name: 'attachments' })
+
// Useful states for our compoennt
// This boolean allow us to control when a video was autocompleted (YouTube for example)
const [isAutocompleted, setIsAutocompleted] = useState(false)
@@ -170,11 +195,20 @@ export const VideoForm = ({ source, video, isUpdate }: Props) => {
}
}
+ /** Need to send only attachments ids */
+ const mappedData = {
+ ...data,
+ attachments: attachments.fields.map((a) => a.id),
+ }
+
try {
// handle response and get video and thumbnail psu url to upload them
const { videoPutPsu, thumbnailPutPsu } = await (isUpdate
- ? updateVideo({ slug: video!.slug, body: data })
- : createVideo(data)
+ ? updateVideo({
+ slug: video!.slug,
+ body: mappedData as unknown as IVideoForm,
+ })
+ : createVideo(mappedData as unknown as IVideoForm)
).unwrap()
if (!isYoutube) {
@@ -337,6 +371,86 @@ export const VideoForm = ({ source, video, isUpdate }: Props) => {
+
+
+
+ {t('forms.create-update.attachments.label')}
+ ('forms.create-update.attachments.add')}>
+ setIsModalAttachmentOpen(true)}
+ color="primary"
+ >
+
+
+
+
+
+ {t('forms.create-update.attachments.description')}
+
+ {attachments.fields.length > 0 ? (
+
+ {attachments.fields.map((attachment, i) => {
+ return (
+ (
+ 'forms.create-update.attachments.remove'
+ )}
+ >
+ attachments.remove(i)}
+ >
+
+
+
+ }
+ >
+
+
+
+
+
+ )
+ })}
+
+ ) : (
+
+ {t('forms.create-update.attachments.empty')}
+
+ )}
+ {isModalAttachmentOpen && (
+ setIsModalAttachmentOpen(false)}
+ />
+ )}
+
+
{t('forms.create-update.title.status')}
diff --git a/src/modules/videos/components/PlayerSidebar/AttachmentsPanel/AttachmentsPanel.component.tsx b/src/modules/videos/components/PlayerSidebar/AttachmentsPanel/AttachmentsPanel.component.tsx
index 13e1f24f..8971bedc 100644
--- a/src/modules/videos/components/PlayerSidebar/AttachmentsPanel/AttachmentsPanel.component.tsx
+++ b/src/modules/videos/components/PlayerSidebar/AttachmentsPanel/AttachmentsPanel.component.tsx
@@ -1,20 +1,11 @@
-import {
- Alert,
- Avatar,
- Link,
- List,
- ListItem,
- Stack,
- Typography,
-} from '@mui/material'
+import { Alert, Link, List, ListItem, ListItemText } from '@mui/material'
import { useTranslation } from 'react-i18next'
import { AutoScrollBox } from '@core/components/AutoScrollBox/AutoScrollBox.component'
-import { Icon } from '@core/components/Icon/Icon.component'
import { Video } from '@videos/models/video.model'
-import { getDomain } from '@attachments/helpers/favicon.helper'
+import { AttachmentAvatar } from '@attachments/components/AttachmentAvatar.component'
import { useGetVideoAttachmentsQuery } from '@attachments/services/attachment.service'
interface AttachmentPanelProps {
@@ -35,25 +26,20 @@ export const AttachmentsPanel = ({ video }: AttachmentPanelProps) => {
) : (
attachments?.items.map((attachment) => (
-
-
-
-
-
-
-
-
- {attachment.title}
-
-
-
-
+
+
+
+
+
))
)
diff --git a/src/modules/videos/models/video.model.ts b/src/modules/videos/models/video.model.ts
index c3fda64a..bea81dfc 100644
--- a/src/modules/videos/models/video.model.ts
+++ b/src/modules/videos/models/video.model.ts
@@ -35,7 +35,7 @@ export interface Video {
// TODO
// tags?: Tag[];
userMeta?: WatchMetadata | undefined
- attachments: Attachment[]
+ attachments?: Attachment[]
watchtime?: Watchtime | undefined
isLiked?: boolean
availableLanguages: SubtitleLanguages[]
diff --git a/src/modules/videos/services/video.service.ts b/src/modules/videos/services/video.service.ts
index 28009875..90953cf2 100644
--- a/src/modules/videos/services/video.service.ts
+++ b/src/modules/videos/services/video.service.ts
@@ -90,7 +90,6 @@ export const videosApi = createApi({
url: Endpoint.Videos,
method: 'POST',
body,
- // body: { ...body, attachments: body.attachments.map((a) => a.id) }, // TODO #416
}),
// Invalidates all video-type queries providing the LIST id - after all, depending of the sort order
// that newly created video could show up in any lists.
@@ -105,7 +104,6 @@ export const videosApi = createApi({
url: `${Endpoint.Videos}/${slug}`,
method: 'PUT',
body,
- // body: { ...body, attachments: body.attachments.map((a) => a.id) }, // TODO #416
}),
// Invalidates all queries that subscribe to this Video `slug` only.
// In this case, `getVideo` will be re-run. `getVideos` *might* rerun, if this id was under its results.