diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.scss b/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.scss index b67f76ff49f..eb39508564c 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.scss +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.scss @@ -20,6 +20,7 @@ .hue-storage-file-page { display: flex; flex: 1; + height: 100%; flex-direction: column; gap: vars.$fluidx-spacing-s; padding: vars.$fluidx-spacing-s 0; @@ -84,20 +85,47 @@ gap: vars.$fluidx-spacing-s; } - .preview__textarea { - resize: none; - width: 100%; - height: 100%; - border: none; - border-radius: 0; - padding: vars.$fluidx-spacing-s; - box-shadow: none; - } + .preview__content { + display: flex; + flex: 1; + justify-content: center; + align-items: center; + background-color: vars.$fluidx-gray-040; + + .preview__textarea { + resize: none; + width: 100%; + height: 100%; + border: none; + border-radius: 0; + padding: vars.$fluidx-spacing-s; + box-shadow: none; + } - .preview__textarea[readonly] { - cursor: text; - color: vars.$fluidx-black; - background-color: vars.$fluidx-white; + .preview__textarea[readonly] { + cursor: text; + color: vars.$fluidx-black; + background-color: vars.$fluidx-white; + } + + .preview__document { + display: flex; + align-items: center; + flex-direction: column; + gap: 16px; + } + + audio { + width: 90%; + } + + video { + height: 90%; + } + + .preview__unsupported { + font-size: vars.$font-size-lg; + } } } } \ No newline at end of file diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.test.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.test.tsx index 31811a6b42c..f1ed41ffa62 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.test.tsx +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.test.tsx @@ -34,8 +34,14 @@ jest.mock('../../../utils/huePubSub', () => ({ publish: jest.fn() })); +const mockSave = jest.fn(); +jest.mock('../../../api/utils', () => ({ + post: () => mockSave() +})); + // Mock data for fileData const mockFileData: PathAndFileData = { + editable: true, path: '/path/to/file.txt', stats: { size: 123456, @@ -50,7 +56,8 @@ const mockFileData: PathAndFileData = { rwx: 'rwxr-xr-x', breadcrumbs: [], view: { - contents: 'Initial file content' + contents: 'Initial file content', + compression: 'none' }, files: [], page: { @@ -92,6 +99,22 @@ describe('StorageFilePage', () => { expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull(); }); + it('hide edit button when editable is false', () => { + render(); + + expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull(); + }); + + it('hide edit button when editable is false', () => { + render( + + ); + + expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull(); + }); + it('shows save and cancel buttons when editing', async () => { const user = userEvent.setup(); render(); @@ -191,4 +214,76 @@ describe('StorageFilePage', () => { expect(screen.queryByRole('button', { name: 'Download' })).toBeNull(); expect(screen.queryByRole('link', { name: 'Download' })).toBeNull(); }); + + it('renders a textarea for text files', () => { + render( + + ); + + const textarea = screen.getByRole('textbox'); + expect(textarea).toBeInTheDocument(); + expect(textarea).toHaveValue('Text file content'); + }); + + it('renders an image for image files', () => { + render( + + ); + + const img = screen.getByRole('img'); + expect(img).toBeInTheDocument(); + expect(img).toHaveAttribute('src', expect.stringContaining('imagefile.png')); + }); + + it('renders a preview button for document files', () => { + render( + + ); + + expect(screen.getByRole('button', { name: /preview document/i })).toBeInTheDocument(); + }); + + it('renders an audio player for audio files', () => { + render( + + ); + + const audio = screen.getByTestId('preview__content__audio'); // audio tag can't be access using getByRole + expect(audio).toBeInTheDocument(); + expect(audio.children[0]).toHaveAttribute('src', expect.stringContaining('audiofile.mp3')); + }); + + it('renders a video player for video files', () => { + render( + + ); + + const video = screen.getByTestId('preview__content__video'); // video tag can't be access using getByRole + expect(video).toBeInTheDocument(); + expect(video.children[0]).toHaveAttribute('src', expect.stringContaining('videofile.mp4')); + }); + + it('displays a message for unsupported file types', () => { + render( + + ); + + expect(screen.getByText(/preview not available for this file/i)).toBeInTheDocument(); + }); }); diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx index fb41d37df45..9bbb6599742 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx @@ -20,8 +20,15 @@ import './StorageFilePage.scss'; import { i18nReact } from '../../../utils/i18nReact'; import Button, { PrimaryButton } from 'cuix/dist/components/Button'; import { getFileMetaData } from './StorageFilePage.util'; -import { DOWNLOAD_API_URL } from '../../../reactComponents/FileChooser/api'; +import { DOWNLOAD_API_URL, SAVE_FILE_API_URL } from '../../../reactComponents/FileChooser/api'; import huePubSub from '../../../utils/huePubSub'; +import useSaveData from '../../../utils/hooks/useSaveData'; +import { + EDITABLE_FILE_FORMATS, + SUPPORTED_FILE_EXTENSIONS, + SupportedFileTypes +} from '../../../utils/constants/storageBrowser'; +import { Spin } from 'antd'; const StorageFilePage = ({ fileData }: { fileData: PathAndFileData }): JSX.Element => { const { t } = i18nReact.useTranslation(); @@ -29,89 +36,173 @@ const StorageFilePage = ({ fileData }: { fileData: PathAndFileData }): JSX.Eleme const [fileContent, setFileContent] = React.useState(fileData.view?.contents); const fileMetaData = useMemo(() => getFileMetaData(t, fileData), [t, fileData]); + const { loading: isSaving, save } = useSaveData(SAVE_FILE_API_URL); + const handleEdit = () => { setIsEditing(true); }; - const handleDownload = () => { - huePubSub.publish('hue.global.info', { message: t('Downloading your file, Please wait...') }); + const handleCancel = () => { + setIsEditing(false); + setFileContent(fileData.view?.contents); }; const handleSave = () => { - // TODO: Save file content to API setIsEditing(false); + save( + { + path: fileData.path, + encoding: 'utf-8', + contents: fileContent + }, + { + onError: () => { + setIsEditing(true); + }, + onSuccess: () => { + huePubSub.publish('hue.global.info', { message: t('Changes saved!') }); + } + } + ); }; - const handleCancel = () => { - setIsEditing(false); - setFileContent(fileData.view?.contents); + const handleDownload = () => { + huePubSub.publish('hue.global.info', { message: t('Downloading your file, Please wait...') }); }; + const filePreviewUrl = `${DOWNLOAD_API_URL}${fileData.path}?disposition=inline`; + + const fileName = fileData?.path?.split('/')?.pop(); + const fileType = useMemo(() => { + const fileExtension = fileName?.split('.')?.pop()?.toLocaleLowerCase(); + if (!fileExtension) { + return SupportedFileTypes.OTHER; + } + return SUPPORTED_FILE_EXTENSIONS[fileExtension] ?? SupportedFileTypes.OTHER; + }, [fileName]); + + const isEditingEnabled = + !isEditing && + fileData.editable && + EDITABLE_FILE_FORMATS[fileType] && + fileData?.view?.compression?.toLocaleLowerCase() === 'none'; + return ( -
-
- {fileMetaData.map((row, index) => ( -
- {row.map(item => ( -
-
{item.label}
-
{item.value}
-
- ))} + +
+
+ {fileMetaData.map((row, index) => ( +
+ {row.map(item => ( +
+
{item.label}
+
{item.value}
+
+ ))} +
+ ))} +
+ +
+
+ {t('Content')} +
+ {isEditingEnabled && ( + + {t('Edit')} + + )} + {isEditing && ( + <> + + {t('Save')} + + + + )} + {fileData.show_download_button && ( + + + {t('Download')} + + + )} +
- ))} -
-
-
- {t('Content')} -
- - - - +
+ {fileType === SupportedFileTypes.TEXT && ( +