Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR implements a profile picture export feature, allowing administrators to download user profile pictures as a ZIP file. The changes affect both frontend and backend components, adding new API endpoints, UI pages, and export functionality.
Changes:
- Added backend endpoint and service method to export profile pictures as a ZIP archive using the archiver library
- Created a dedicated frontend admin page for selecting and exporting specific user profile pictures
- Integrated profile picture export into existing admin interfaces (roles page and period applications table)
- Changed ProfilePictureStatus enum values from Hungarian descriptions to English constants for consistency with the backend
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/frontend/src/types/user-entity.ts | Changed ProfilePictureStatus enum from Hungarian text values to English constants (ACCEPTED, PENDING, REJECTED) |
| apps/frontend/src/middleware.ts | Added /admin route and subpaths to authentication middleware protection |
| apps/frontend/src/lib/profile-pictures.ts | New utility function for triggering profile picture export API call and file download |
| apps/frontend/src/components/ui/generating-dialog.tsx | Removed unused DialogTitle import and element |
| apps/frontend/src/components/ui/UserCard.tsx | Added mutateUsers prop, authSchId display, improved alt text, and visual indicators for profile picture status |
| apps/frontend/src/app/roles/page.tsx | Added export all profile pictures button and mutateUsers callback |
| apps/frontend/src/app/periods/[id]/page.tsx | Added profile picture export handler for selected applications |
| apps/frontend/src/app/periods/[id]/data-table.tsx | Added menu item to export profile pictures for selected applications |
| apps/frontend/src/app/admin/profile-picture-export/page.tsx | New admin page with checkbox-based UI for selective profile picture export |
| apps/backend/src/user/user.service.ts | Implemented exportProfilePictures method using archiver to create ZIP files |
| apps/backend/src/user/user.controller.ts | Added POST endpoint for profile picture export with proper headers; removed Query import |
| apps/backend/package.json | Added archiver dependency and @types/archiver dev dependency |
| .idea/jsLinters/eslint.xml | Updated ESLint configuration path |
Files not reviewed (1)
- .idea/jsLinters/eslint.xml: Language not supported
Comments suppressed due to low confidence (1)
apps/backend/src/user/user.controller.ts:16
- The Query decorator is being used on lines 39, 80, 85, and 86, but it's not imported. The removal of the Query import from the
@nestjs/commonimports will cause a compilation error. Add Query back to the import statement on line 3.
import {
Body,
Controller,
Delete,
Get,
Header,
Param,
ParseIntPipe,
Patch,
Post,
StreamableFile,
UploadedFile,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
apps/frontend/src/app/roles/page.tsx
Outdated
| import { LuDownload } from 'react-icons/lu'; | ||
|
|
||
| export default function Page() { | ||
| const router = useRouter(); |
There was a problem hiding this comment.
The router constant is declared but never used in the component. This is an unused variable that should be removed to keep the code clean.
| ACCEPTED = 'ACCEPTED', | ||
| PENDING = 'PENDING', | ||
| REJECTED = 'REJECTED', |
There was a problem hiding this comment.
The ProfilePictureStatus enum values have been changed from Hungarian descriptions ('Jóváhagyott profilkép', 'Elbírálás alatt álló profilkép', 'Elutasított profilkép') to English constants ('ACCEPTED', 'PENDING', 'REJECTED'). This change affects the tooltip display in PfpStatusBadge.tsx (line 35), which directly shows the enum value. The tooltip will now display English text like "ACCEPTED" instead of the previous Hungarian descriptions. If the UI should remain in Hungarian, consider adding a separate mapping function to translate these enum values for display purposes.
| ACCEPTED = 'ACCEPTED', | |
| PENDING = 'PENDING', | |
| REJECTED = 'REJECTED', | |
| ACCEPTED = 'Jóváhagyott profilkép', | |
| PENDING = 'Elbírálás alatt álló profilkép', | |
| REJECTED = 'Elutasított profilkép', |
| }); | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| const archive = archiver('zip', { zlib: { level: 0 } }); |
There was a problem hiding this comment.
The archiver is configured with compression level 0 (zlib: { level: 0 }), which means no compression. This will result in larger ZIP files. While this may be intentional for faster processing, consider using a moderate compression level (e.g., level 6) to balance file size and performance, especially if many profile pictures are being exported.
| const archive = archiver('zip', { zlib: { level: 0 } }); | |
| const archive = archiver('zip', { zlib: { level: 6 } }); |
|
|
||
| for (const picture of pictures) { | ||
| const imageBuffer = Buffer.from(picture.profileImage.buffer); | ||
| const safeName = picture.user.fullName.replace(/[^\w\s\-áéíóöőúüűÁÉÍÓÖŐÚÜŰ]/g, '_'); |
There was a problem hiding this comment.
The safeName sanitization removes potentially problematic characters, but it doesn't handle the case where the fullName might be empty or only contain special characters, which would result in an empty filename. Consider adding a fallback to use authSchId if the sanitized name is empty, for example: const safeName = picture.user.fullName.replace(/[^\w\s\-áéíóöőúüűÁÉÍÓÖŐÚÜŰ]/g, '_').trim() || picture.user.authSchId;
| const safeName = picture.user.fullName.replace(/[^\w\s\-áéíóöőúüűÁÉÍÓÖŐÚÜŰ]/g, '_'); | |
| const safeName = | |
| picture.user.fullName.replace(/[^\w\s\-áéíóöőúüűÁÉÍÓÖŐÚÜŰ]/g, '_').trim() || | |
| picture.user.authSchId; |
| await props.mutateUsers(); | ||
| } | ||
|
|
||
| // @ts-ignore |
There was a problem hiding this comment.
The @ts-ignore comment is suppressing TypeScript errors without explanation. This should be removed and the underlying TypeScript error should be properly fixed. If there's a legitimate reason to suppress the error, it should be documented with a comment explaining why.
| // @ts-ignore |
| for (const picture of pictures) { | ||
| const imageBuffer = Buffer.from(picture.profileImage.buffer); | ||
| const safeName = picture.user.fullName.replace(/[^\w\s\-áéíóöőúüűÁÉÍÓÖŐÚÜŰ]/g, '_'); | ||
| archive.append(imageBuffer, { name: `${safeName}.jpg` }); |
There was a problem hiding this comment.
Multiple users with the same fullName will have filename collisions in the exported ZIP file. When two users have identical names, only one file will be preserved in the archive. Consider including the authSchId in the filename to ensure uniqueness, for example: ${safeName}_${picture.user.authSchId}.jpg
| archive.append(imageBuffer, { name: `${safeName}.jpg` }); | |
| archive.append(imageBuffer, { name: `${safeName}_${picture.user.authSchId}.jpg` }); |
| const response = await api.post( | ||
| 'users/profile-pictures/export', | ||
| { userIds: userIds ?? [] }, | ||
| { responseType: 'blob' } | ||
| ); | ||
| const objectUrl = URL.createObjectURL(new Blob([response.data], { type: 'application/zip' })); | ||
| const link = document.createElement('a'); | ||
| link.href = objectUrl; | ||
| link.download = 'profile-pictures.zip'; | ||
| document.body.appendChild(link); | ||
| link.click(); | ||
| document.body.removeChild(link); | ||
| URL.revokeObjectURL(objectUrl); |
There was a problem hiding this comment.
The exportProfilePictures function lacks error handling. If the API request fails, the error will propagate to the caller without any user-facing feedback. Consider wrapping the API call in a try-catch block and displaying a toast notification on error, consistent with error handling patterns seen elsewhere in the codebase.
| const response = await api.post( | |
| 'users/profile-pictures/export', | |
| { userIds: userIds ?? [] }, | |
| { responseType: 'blob' } | |
| ); | |
| const objectUrl = URL.createObjectURL(new Blob([response.data], { type: 'application/zip' })); | |
| const link = document.createElement('a'); | |
| link.href = objectUrl; | |
| link.download = 'profile-pictures.zip'; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| URL.revokeObjectURL(objectUrl); | |
| try { | |
| const response = await api.post( | |
| 'users/profile-pictures/export', | |
| { userIds: userIds ?? [] }, | |
| { responseType: 'blob' } | |
| ); | |
| const objectUrl = URL.createObjectURL(new Blob([response.data], { type: 'application/zip' })); | |
| const link = document.createElement('a'); | |
| link.href = objectUrl; | |
| link.download = 'profile-pictures.zip'; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| URL.revokeObjectURL(objectUrl); | |
| } catch (error) { | |
| // Provide user-facing feedback if the export fails. | |
| window.alert('Failed to export profile pictures. Please try again.'); | |
| // Optional: keep logging for debugging purposes. | |
| // eslint-disable-next-line no-console | |
| console.error('Error exporting profile pictures:', error); | |
| throw error; | |
| } |
No description provided.