Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#35): add attachments to video form #36

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/polyflix/public/locales/en/attachments.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
}
},
"errors": {}
},
"selector": {
"title": "Select attachments",
"validate": "Close"
}
},
"closeModal": "Close",
Expand Down
12 changes: 7 additions & 5 deletions apps/polyflix/public/locales/en/videos.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
},
Expand Down
6 changes: 5 additions & 1 deletion apps/polyflix/public/locales/fr/attachments.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
}
},
"errors": {}
},
"selector": {
"title": "Veuillez sélectionner les pièces jointes",
"validate": "Fermer"
}
},
"closeModal": "Fermer",
Expand All @@ -44,4 +48,4 @@
"actions": {
"copyToClipboard": "Copier le lien dans le presse-papier"
}
}
}
12 changes: 7 additions & 5 deletions apps/polyflix/public/locales/fr/videos.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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')
Comment on lines +16 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use only one useTranslation('user') or useTranslation('attachements') then if you want to change use namespace

t('profile.tabs.attachments.content.list.clipboard', { ns: 'user' })

const snackbarService = useInjection<SnackbarService>(SnackbarService)

const avatarContent = () => (
<Avatar src={'' /* TODO : Issue #466 */}>
<Icon name="eva:link-outline" size={30} />
</Avatar>
)

if (copyToClipboard) {
return (
<ListItemIcon>
<CopyToClipboard
onCopy={() => {
snackbarService.createSnackbar(
tUsers('profile.tabs.attachments.content.list.clipboard'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tUsers('profile.tabs.attachments.content.list.clipboard'),
t('profile.tabs.attachments.content.list.clipboard', { ns: 'user' }),

{
variant: 'success',
}
)
}}
text={url}
>
<Tooltip title={tAttachments<string>('actions.copyToClipboard')}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Tooltip title={tAttachments<string>('actions.copyToClipboard')}>
<Tooltip title={t<string>('actions.copyToClipboard')}>

<IconButton>{avatarContent()}</IconButton>
</Tooltip>
</CopyToClipboard>
</ListItemIcon>
)
} else {
return <ListItemIcon>{avatarContent()}</ListItemIcon>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
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<IVideoForm, 'attachments'>
videoId?: string
isOpen: boolean
onClose: () => void
sx?: SxProps<Theme>
}
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<AttachmentParams>({
page,
pageSize: 10,
userId: user!.id,
})

const { data, isLoading, refetch } = 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(() => {
/* Since the attachments are not invalidated after a video update, we need to refetch them here */
if (data) refetch()
}, [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}, [])
}, [data])


useEffect(() => {
/* On update mode, append the previously selected attachments in order to set the checkboxes ticked */
if (videoId && data) {
for (const f of fields) {
if (
f.videos.includes(videoId) &&
!fields.find(({ id }) => id === f.id)
) {
append(f)
}
}
}
}, [videoId, data])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe

Suggested change
}, [videoId, data])
}, [videoId, data, fields])


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 <Redirect push to="/users/profile/attachments/create" />

return (
<Modal
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 2,
...sxProps,
}}
open={isOpen}
onClose={() => onClose()}
aria-labelledby="element modal"
closeAfterTransition
BackdropProps={{
timeout: 500,
}}
>
<Fade in={isOpen}>
<Paper
sx={{
width: {
lg: '40%',
md: '50%',
sm: '70%',
xs: '90%',
},
bgcolor: 'background.default',
borderRadius: 2,
p: {
sm: 2,
xs: 1,
},
}}
variant="outlined"
>
<Typography variant="h4" sx={{ mb: '2%' }}>
{t('forms.selector.title')}
</Typography>
<Scrollbar
sx={{
maxHeight: (theme) => `calc(100vh - ${theme.spacing(30)})`,
minHeight: '300px',
}}
>
<Box sx={{ mt: 2 }}>
{{ data }
? data?.items.map((item) => (
<ListItem
key={item.id}
secondaryAction={
<Checkbox
edge="end"
onChange={handleToggle(item)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better like that if you use the input of the function handleToggle

Suggested change
onChange={handleToggle(item)}
onChange={() => handleToggle(item)}

checked={isAttachmentSelected(item)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, but i'm not sure

/>
}
disablePadding
>
<ListItemButton onClick={handleToggle(item)}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, but i'm not sure

<AttachmentAvatar url={item.url} />
<Link
href={item.url}
target="_blank"
rel="noopener"
color="inherit"
underline="hover"
>
<ListItemText primary={item.title} />
</Link>
</ListItemButton>
</ListItem>
))
: buildSkeletons(3)}
</Box>
</Scrollbar>
<Stack spacing={0}>
{!isLoading &&
(data &&
data.items.length > 0 &&
data.items.length < data.totalCount ? (
Comment on lines +178 to +181
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can group everything into one function for clarity

<Box display="flex" sx={{ mt: 3 }} justifyContent="center">
<PaginationSynced
filters={filters}
setFilters={setFilters}
pageCount={Math.ceil(data?.totalCount / filters.pageSize)}
/>
</Box>
) : (
!data ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can group everything into one function for clarity

(data.items.length === 0 && (
<NoData
variant="attachments"
link="/users/profile/attachments/create"
/>
))
))}
</Stack>
<Container
sx={{ display: 'flex', justifyContent: 'center', my: '1em' }}
>
<Button onClick={onClose} variant="contained">
{t('forms.selector.validate')}
</Button>
</Container>
</Paper>
</Fade>
</Modal>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const attachmentsApi = createApi({
}),
getVideoAttachments: builder.query<PaginatedAttachments, string>({
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Todo ?

},
providesTags: (result) =>
result
Expand Down
Loading