diff --git a/src/components/icon-extension-with-title/IconExtensionWithTitle.styles.ts b/src/components/icon-extension-with-title/IconExtensionWithTitle.styles.ts index d1872e5cf..69d9692b9 100644 --- a/src/components/icon-extension-with-title/IconExtensionWithTitle.styles.ts +++ b/src/components/icon-extension-with-title/IconExtensionWithTitle.styles.ts @@ -18,6 +18,23 @@ export const styles = { fontSize: '12px', typography: TypographyVariantEnum.Caption }, + inputWithIcons: { + display: 'flex', + columnGap: '10px', + flex: 1 + }, + input: { + flex: 1, + typography: TypographyVariantEnum.Subtitle2 + }, + actionIcon: (color: string) => ({ + width: '16px', + height: '16px', + p: '4px', + borderRadius: '6px', + color: `${color}.700`, + backgroundColor: `${color}.100` + }), titleWithDescription: { title: { typography: TypographyVariantEnum.Subtitle2 diff --git a/src/components/icon-extension-with-title/IconExtensionWithTitle.tsx b/src/components/icon-extension-with-title/IconExtensionWithTitle.tsx index 73f4d4ac4..6804988b0 100644 --- a/src/components/icon-extension-with-title/IconExtensionWithTitle.tsx +++ b/src/components/icon-extension-with-title/IconExtensionWithTitle.tsx @@ -1,38 +1,94 @@ -import { FC } from 'react' +import { FC, useState, useRef, useEffect, ChangeEvent } from 'react' import { useTranslation } from 'react-i18next' +import IconButton from '@mui/material/IconButton' +import ClearIcon from '@mui/icons-material/Clear' +import DoneIcon from '@mui/icons-material/Done' +import InputBase from '@mui/material/InputBase' import Box from '@mui/material/Box' import TitleWithDescription from '~/components/title-with-description/TitleWithDescription' -import { styles } from '~/components/icon-extension-with-title/IconExtensionWithTitle.styles' +import { snackbarVariants } from '~/constants' import { convertBytesToProperFormat } from '~/utils/helper-functions' +import { styles } from '~/components/icon-extension-with-title/IconExtensionWithTitle.styles' interface IconExtensionWithTitleProps { title: string size?: number + isEditable?: boolean + onCancel?: () => void + onSave?: (fileName: string) => Promise } const IconExtensionWithTitle: FC = ({ title, - size + size, + isEditable = false, + onCancel, + onSave }) => { const { t } = useTranslation() + const inputRef = useRef(null) + const initialInputValue = title.substring(0, title.lastIndexOf('.')) + const [inputValue, setInputValue] = useState(initialInputValue) + const [fileExtension] = title.split('.').reverse() - const fileExtension = title.slice(title.lastIndexOf('.') + 1) const convertSize = (incomingSize: number) => { const { size: properSize, unit } = convertBytesToProperFormat(incomingSize) return properSize + ' ' + t(`common.${unit}`) } + const onChange = (e: ChangeEvent) => { + setInputValue(e.target.value) + } + + const onCancelHandler = () => { + onCancel?.() + } + + const onSaveHandler = () => { + const trimmedValue = inputValue.trim() + const value = trimmedValue !== initialInputValue ? trimmedValue : '' + onSave && void onSave(value) + } + + useEffect(() => { + inputRef?.current?.select() + !isEditable && setInputValue(initialInputValue) + }, [isEditable, initialInputValue]) + + const actionIcons = ( + + + + + + + + + ) + return ( {fileExtension} - + {isEditable ? ( + + + {actionIcons} + + ) : ( + + )} ) } diff --git a/src/constants/request.ts b/src/constants/request.ts index a797c3183..875e4fa00 100644 --- a/src/constants/request.ts +++ b/src/constants/request.ts @@ -53,6 +53,7 @@ export const URLs = { }, attachments: { get: '/attachments', + patch: '/attachments', delete: '/attachments' } }, diff --git a/src/constants/translations/en/common.json b/src/constants/translations/en/common.json index 283751464..5bc8cd83b 100644 --- a/src/constants/translations/en/common.json +++ b/src/constants/translations/en/common.json @@ -12,6 +12,7 @@ "search": "Search", "edit": "Edit", "delete": "Delete", + "rename": "Rename", "showMore": "Show more", "showLess": "Show less", "confirmButton": "OK", diff --git a/src/constants/translations/en/my-resources-page.json b/src/constants/translations/en/my-resources-page.json index 644c01e6e..cace7e5e5 100644 --- a/src/constants/translations/en/my-resources-page.json +++ b/src/constants/translations/en/my-resources-page.json @@ -24,7 +24,8 @@ "lastUpdate": "Last update", "emptyAttachments": "You have no attachments yet", "confirmAttachmentDeletionTitle":"Do you confirm attachment deletion?", - "successDeletion":"Attachment was deleted successfully" + "successDeletion":"Attachment was deleted successfully", + "validationError": "File name cannot be longer than 55 symbol." }, "quizzes": { "newQuizBtn": "New quiz", diff --git a/src/constants/translations/ua/common.json b/src/constants/translations/ua/common.json index 1b1f25588..195321f46 100644 --- a/src/constants/translations/ua/common.json +++ b/src/constants/translations/ua/common.json @@ -11,6 +11,8 @@ "of": "З", "search": "Пошук", "edit":"Редагувати", + "delete": "Видалити", + "rename": "Перейменувати", "showMore": "Показати більше", "showLess": "Показати менше", "uah": "ГРН", diff --git a/src/constants/translations/ua/my-resources-page.json b/src/constants/translations/ua/my-resources-page.json index 96680ab37..722a9dbb8 100644 --- a/src/constants/translations/ua/my-resources-page.json +++ b/src/constants/translations/ua/my-resources-page.json @@ -25,7 +25,8 @@ "emptyAttachments": "У вас ще немає вкладень", "confirmAttachmentDeletionTitle":"Ви підтверджуєте видалення вкладення?", "successDeletion":"Вкладення було успішно видалено", - "addAttachment": "Добавити файл" + "addAttachment": "Добавити файл", + "validationError": "Назва файлу не може бути довшою за 55 символів." }, "quizzes": { "newQuizBtn": "Новий тест", diff --git a/src/containers/my-resources/attachments-container/AttachmentsContainer.constants.tsx b/src/containers/my-resources/attachments-container/AttachmentsContainer.constants.tsx index 2b9afee19..e508377e3 100644 --- a/src/containers/my-resources/attachments-container/AttachmentsContainer.constants.tsx +++ b/src/containers/my-resources/attachments-container/AttachmentsContainer.constants.tsx @@ -11,13 +11,22 @@ import { } from '~/types' import { styles } from '~/containers/my-resources/attachments-container/AttachmentsContainer.styles' -export const columns: TableColumn[] = [ +export const columns = ( + selectedItemId: string, + onCancel: () => void, + onSave: (fileName: string) => Promise +): TableColumn[] => [ { label: 'myResourcesPage.attachments.file', field: 'fileName', - calculatedCellValue: (item: Attachment) => { - return - } + calculatedCellValue: (item: Attachment) => ( + + ) }, { label: 'myResourcesPage.attachments.size', diff --git a/src/containers/my-resources/attachments-container/AttachmentsContainer.styles.ts b/src/containers/my-resources/attachments-container/AttachmentsContainer.styles.ts index 4c1631e32..cf5b8cb16 100644 --- a/src/containers/my-resources/attachments-container/AttachmentsContainer.styles.ts +++ b/src/containers/my-resources/attachments-container/AttachmentsContainer.styles.ts @@ -24,7 +24,12 @@ export const styles = { }, addAttachmentBtn: { width: 'fit-content' }, addAttachmentIcon: { ml: '5px', width: { xs: '18px', sm: '22px' } }, - table: roundedBorderTable, + table: { + ...roundedBorderTable, + '& td,th': { + '&:first-of-type': { maxWidth: '50%', width: '100%' } + } + }, sizeTitle: captionTitle, dateTitle: captionTitle } diff --git a/src/containers/my-resources/attachments-container/AttachmentsContainer.tsx b/src/containers/my-resources/attachments-container/AttachmentsContainer.tsx index 0c4ca6ec2..04a76984e 100644 --- a/src/containers/my-resources/attachments-container/AttachmentsContainer.tsx +++ b/src/containers/my-resources/attachments-container/AttachmentsContainer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'react' +import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Box from '@mui/material/Box' @@ -15,7 +15,6 @@ import { useSnackBarContext } from '~/context/snackbar-context' import { ResourceService } from '~/services/resource-service' import { authRoutes } from '~/router/constants/authRoutes' -import { defaultResponses, snackbarVariants } from '~/constants' import { columns, initialSort, @@ -23,7 +22,13 @@ import { removeColumnRules } from '~/containers/my-resources/attachments-container/AttachmentsContainer.constants' import { ajustColumns, getScreenBasedLimit } from '~/utils/helper-functions' -import { ItemsWithCount, Attachment, ErrorResponse } from '~/types' +import { defaultResponses, snackbarVariants } from '~/constants' +import { + ItemsWithCount, + Attachment, + ErrorResponse, + UpdateAttachmentParams +} from '~/types' import { styles } from '~/containers/my-resources/attachments-container/AttachmentsContainer.styles' const AttachmentsContainer = () => { @@ -31,36 +36,27 @@ const AttachmentsContainer = () => { const { setAlert } = useSnackBarContext() const { openDialog } = useConfirm() const { page, handleChangePage } = usePagination() - - const sortOptions = useSort({ initialSort }) - const { sort } = sortOptions - const breakpoints = useBreakpoints() - const itemsPerPage = getScreenBasedLimit(breakpoints, itemsLoadLimit) - + const sortOptions = useSort({ initialSort }) const searchFileName = useRef('') + const [selectedItemId, setSelectedItemId] = useState('') - const getAttachments = useCallback( - () => - ResourceService.getAttachments({ - limit: itemsPerPage, - skip: (page - 1) * itemsPerPage, - sort, - fileName: searchFileName.current - }), - [itemsPerPage, page, sort, searchFileName] - ) + const { sort } = sortOptions + const itemsPerPage = getScreenBasedLimit(breakpoints, itemsLoadLimit) const onAttachmentError = useCallback( - (error: ErrorResponse) => { + (error: ErrorResponse, message?: string) => { setAlert({ severity: snackbarVariants.error, - message: error ? `errors.${error.code}` : '' + message: error ? message ?? `errors.${error.code}` : '' }) }, [setAlert] ) + const onAttachmentUpdateError = (error: ErrorResponse) => + onAttachmentError(error, 'myResourcesPage.attachments.validationError') + const onDeleteAttachmentError = (error: ErrorResponse) => { setAlert({ severity: snackbarVariants.error, @@ -75,11 +71,28 @@ const AttachmentsContainer = () => { }) } + const getAttachments = useCallback( + () => + ResourceService.getAttachments({ + limit: itemsPerPage, + skip: (page - 1) * itemsPerPage, + sort, + fileName: searchFileName.current + }), + [itemsPerPage, page, sort, searchFileName] + ) + const deleteAttachment = useCallback( (id?: string) => ResourceService.deleteAttachment(id ?? ''), [] ) + const updateAttachment = useCallback( + (params?: UpdateAttachmentParams) => + ResourceService.updateAttachment(params), + [] + ) + const { response, loading, @@ -90,6 +103,19 @@ const AttachmentsContainer = () => { onResponseError: onAttachmentError }) + const onAttachmentUpdate = useCallback( + () => void fetchGetAttachments(), + [fetchGetAttachments] + ) + + const { fetchData: updateData } = useAxios({ + service: updateAttachment, + defaultResponse: null, + onResponseError: onAttachmentUpdateError, + onResponse: onAttachmentUpdate, + fetchOnMount: false + }) + const { error, fetchData: fetchDeleteAttachment } = useAxios({ service: deleteAttachment, fetchOnMount: false, @@ -98,6 +124,13 @@ const AttachmentsContainer = () => { onResponse: onDeleteAttachmentResponse }) + const onSave = async (fileName: string) => { + if (fileName) await updateData({ id: selectedItemId, fileName }) + setSelectedItemId('') + } + const onEdit = (id: string) => setSelectedItemId(id) + const onCancel = () => setSelectedItemId('') + const handleDeleteAttachment = async (id: string, isConfirmed: boolean) => { if (isConfirmed) { await fetchDeleteAttachment(id) @@ -114,11 +147,16 @@ const AttachmentsContainer = () => { }) } - const columnsToShow = ajustColumns(breakpoints, columns, removeColumnRules) + const columnsToShow = ajustColumns( + breakpoints, + columns(selectedItemId, onCancel, onSave), + removeColumnRules + ) + const rowActions = [ { - label: t('common.edit'), - func: () => console.log(t('common.edit')) + label: t('common.rename'), + func: onEdit }, { label: t('common.delete'), diff --git a/src/services/resource-service.ts b/src/services/resource-service.ts index b0f182eb2..2b41d4194 100644 --- a/src/services/resource-service.ts +++ b/src/services/resource-service.ts @@ -9,7 +9,8 @@ import { GetLessonsParams, ItemsWithCount, LessonData, - Lesson + Lesson, + UpdateAttachmentParams } from '~/types' import { createUrlPath } from '~/utils/helper-functions' @@ -33,6 +34,11 @@ export const ResourceService = { params?: Partial ): Promise>> => await axiosClient.get(URLs.resources.attachments.get, { params }), + updateAttachment: async (params?: UpdateAttachmentParams) => + await axiosClient.patch( + createUrlPath(URLs.resources.attachments.patch, params?.id), + params + ), deleteAttachment: async (id: string): Promise => await axiosClient.delete( createUrlPath(URLs.resources.attachments.delete, id) diff --git a/src/types/attachment/interfaces/attachment.interface.ts b/src/types/attachment/interfaces/attachment.interface.ts index e23ecd771..63aebe465 100644 --- a/src/types/attachment/interfaces/attachment.interface.ts +++ b/src/types/attachment/interfaces/attachment.interface.ts @@ -7,3 +7,8 @@ export interface Attachment extends CommonEntityFields { description: string size: number } + +export interface UpdateAttachmentParams { + fileName: Attachment['fileName'] + id: Attachment['author']['_id'] +}