Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
28eb263
chore: changing the settings of the file component
Tshepo1103-lab Mar 26, 2025
eb70cd4
chore: changing the file to show thumbnail
Tshepo1103-lab Mar 27, 2025
5c82948
chore: adding styling to the thumbnail
Tshepo1103-lab Mar 28, 2025
924b2ae
thumbnail things
czwe-01 Apr 1, 2025
46e8b30
fix: importing the download icon
Tshepo1103-lab Apr 2, 2025
6761289
chore: dynamically showing file controls
Tshepo1103-lab Apr 4, 2025
a56b2f4
chore: started code refactoring
Tshepo1103-lab Apr 4, 2025
fd900b2
fix: fix the dragger stub
Tshepo1103-lab Apr 4, 2025
5c65ba3
Merge remote-tracking branch 'origin' into tshepo/en/file-component
Tshepo1103-lab Apr 4, 2025
4cf2050
Merge branch 'main' into tshepo/en/file-component
Tshepo1103-lab Apr 6, 2025
5b3b4ba
chore: passing down styles into the Dragger stub component
Tshepo1103-lab Apr 6, 2025
a4161ce
fix: fixign the replace functionality
Tshepo1103-lab Apr 7, 2025
3c2191f
Merge branch 'main' into tshepo/en/file-component
Tshepo1103-lab Apr 7, 2025
bad033e
chore: removing unused code
Tshepo1103-lab Apr 7, 2025
3981ce4
chore: showing download if type is not image
Tshepo1103-lab Apr 8, 2025
49e2641
fix: fixing the style of the thumbnail
Tshepo1103-lab Apr 9, 2025
4ccdda4
chore: changed the order of the settings
Tshepo1103-lab Apr 10, 2025
62e867e
chore: changing the download to be on the label
Tshepo1103-lab Apr 10, 2025
734c3f2
chore: removing the white background on default styles
Tshepo1103-lab Apr 10, 2025
6e93b31
chore: adding a grid template
Tshepo1103-lab Apr 10, 2025
acd1eb1
chore: changing the casing of API URL
Tshepo1103-lab Apr 13, 2025
ad0c74a
chore: hiding the file name by default
Tshepo1103-lab Apr 13, 2025
f886446
chore: removing the download button when file name is visible
Tshepo1103-lab Apr 14, 2025
e38aa35
chore: cleaning up the code with error handling
Tshepo1103-lab Apr 14, 2025
33efe05
Merge branch 'main' of https://github.com/Tshepo1103-lab/shesha-frame…
Tshepo1103-lab Apr 14, 2025
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
262 changes: 207 additions & 55 deletions shesha-reactjs/src/components/fileUpload/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import React, { FC, useRef } from 'react';
import { useStoredFile } from '@/providers';
import { Upload, App, Button } from 'antd';
import { UploadRequestOption as RcCustomRequestOptions } from 'rc-upload/lib/interface';
import { listType } from '@/designer-components/attachmentsEditor/attachmentsEditor';
import { getFileIcon, isImageType } from '@/icons/fileIcons';
import { useSheshaApplication, useStoredFile, useTheme } from '@/providers';
import {
DeleteOutlined,
DownloadOutlined,
EyeOutlined,
InfoCircleOutlined,
PictureOutlined,
SyncOutlined,
DeleteOutlined,
LoadingOutlined,
UploadOutlined,
} from '@ant-design/icons';
import { App, Button, Space, Upload } from 'antd';
import { Image } from 'antd/lib';
import { UploadProps } from 'antd/lib/upload/Upload';
import filesize from 'filesize';
import { FileVersionsPopup } from './fileVersionsPopup';
import { UploadRequestOption as RcCustomRequestOptions } from 'rc-upload/lib/interface';
import React, { FC, useEffect, useRef, useState } from 'react';
import FileVersionsPopup from './fileVersionsPopup';
import { DraggerStub } from './stubs';
import { useStyles } from './styles/styles';
import classNames from 'classnames';

const { Dragger } = Upload;

export interface IFileUploadProps {
Expand All @@ -29,6 +32,12 @@ export interface IFileUploadProps {
isStub?: boolean;
allowedFileTypes?: string[];
isDragger?: boolean;
listType?: listType;
thumbnailWidth?: string;
thumbnailHeight?: string;
borderRadius?: number;
hideFileName?: boolean;
styles?: any;
}

export const FileUpload: FC<IFileUploadProps> = ({
Expand All @@ -40,6 +49,9 @@ export const FileUpload: FC<IFileUploadProps> = ({
isStub = false,
allowedFileTypes = [],
isDragger = false,
listType = 'text',
hideFileName = false,
styles: stylesProp,
}) => {
const {
fileInfo,
Expand All @@ -51,26 +63,56 @@ export const FileUpload: FC<IFileUploadProps> = ({
succeeded: { downloadZip: downloadZipSuccess },
*/
} = useStoredFile();
const { styles } = useStyles();
const { backendUrl, httpHeaders } = useSheshaApplication();

const props = {
style: stylesProp,
model: {
layout: listType === 'thumbnail' && !isDragger,
isDragger,
hideFileName,
},
};
const { styles } = useStyles(props);
const { theme } = useTheme();
const uploadButtonRef = useRef(null);
const uploadDraggerSpanRef = useRef(null);
const { message } = App.useApp();
const [imageUrl, setImageUrl] = useState('');
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState({ url: '', uid: '', name: '' });

const url = fileInfo?.url ? `${backendUrl}${fileInfo.url}` : '';
useEffect(() => {
if (fileInfo && url) {
fetch(url, { headers: { ...httpHeaders, 'Content-Type': 'application/octet-stream' } })
.then((response) => {
return response.blob();
})
.then((blob) => {
return URL.createObjectURL(blob);
})
.then((url) => {
setImageUrl(url);
});
}
}, [fileInfo]);

const onCustomRequest = ({ file /*, onError, onSuccess*/ }: RcCustomRequestOptions) => {
// call action from context
uploadFile({ file: file as File }, callback);
};

const onDownloadClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault();
downloadFile({ fileId: fileInfo.id, fileName: fileInfo.name });
};

const onReplaceClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault();
e.stopPropagation();

if (!isDragger) {
uploadButtonRef.current.click();
// Find the hidden input element created by Upload component
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
if (input) {
input.click();
}
} else {
if (uploadDraggerSpanRef.current) {
uploadDraggerSpanRef.current.click();
Expand All @@ -83,26 +125,105 @@ export const FileUpload: FC<IFileUploadProps> = ({
deleteFile();
};

const fileControls = () => {
const onPreview = () => {
if (fileInfo) {
if (!url) {
message.error('Preview URL not available');
return;
}
setPreviewImage({ url, uid: fileInfo?.id, name: fileInfo?.name });
setPreviewOpen(true);
}
};

const fileControls = (color: string) => {
return (
<React.Fragment>
{fileInfo && (
<a className={styles.shaUploadHistoryControl}>
{false && <InfoCircleOutlined />}
<FileVersionsPopup fileId={fileInfo.id} />
</a>
)}
//space between the icons
<Space>
<a style={{ color: color }}>
{false && <InfoCircleOutlined />}
<FileVersionsPopup fileId={fileInfo?.id} />
</a>
{allowReplace && (
<a className={styles.shaUploadReplaceControl} onClick={onReplaceClick}>
<a onClick={onReplaceClick} style={{ color: color }}>
<SyncOutlined title="Replace" />
</a>
)}

{allowDelete && (
<a className={styles.shaUploadRemoveControl} onClick={onDeleteClick}>
<a onClick={(e) => onDeleteClick(e)} style={{ color: color }}>
<DeleteOutlined title="Remove" />
</a>
)}
</React.Fragment>
{isImageType(fileInfo?.type) ? (
<a onClick={onPreview} style={{ color: color }}>
<EyeOutlined title="Preview" />
</a>
) : (
hideFileName && (
<a
onClick={() => downloadFile({ fileId: fileInfo?.id, fileName: fileInfo?.name })}
style={{ color: color }}
>
<DownloadOutlined title="Download" />
</a>
)
)}
</Space>
);
};

const iconRender = (fileInfo) => {
const { type, name } = fileInfo;
if (isImageType(type)) {
if (listType === 'thumbnail' && !isDragger) {
return <Image src={imageUrl} alt={name} preview={false} className={styles.thumbnailControls} />;
}
}

return getFileIcon(type);
};

const styledfileControls = () => {
return (
fileInfo && (
<div className={styles.styledFileControls}>
{iconRender(fileInfo)}
<div className={styles.overlayThumbnailControls} style={{ fontSize: '15px' }}>
{fileControls('#fff')}
</div>
</div>
)
);
};

const renderFileItem = (file: any) => {
const showThumbnailControls = !isUploading && listType === 'thumbnail';
const showTextControls = listType === 'text';

return (
<div>
{showThumbnailControls && styledfileControls()}
<a title={file.name}>
<Space>
{isUploading ? (
<span>
<SyncOutlined spin />
</span>
) : (
<div className="thumbnail-item-name">
{(listType === 'text' || !hideFileName) && (
<a
style={{ marginRight: '5px' }}
onClick={() => downloadFile({ fileId: file.id, fileName: file.name })}
>{`${file.name} (${filesize(file.size)})`}</a>
)}
{showTextControls && fileControls(theme.application.primaryColor)}
</div>
)}
</Space>
</a>
</div>
);
};

Expand All @@ -112,6 +233,7 @@ export const FileUpload: FC<IFileUploadProps> = ({
accept: allowedFileTypes?.join(','),
multiple: false,
fileList: fileInfo ? [fileInfo] : [],
style: stylesProp,
customRequest: onCustomRequest,
onChange(info) {
if (info.file.status !== 'uploading') {
Expand All @@ -123,63 +245,93 @@ export const FileUpload: FC<IFileUploadProps> = ({
message.error(`${info.file.name} file upload failed.`);
}
},
itemRender: (_originNode, file, _currFileList) => {
return (
<div className={file.error ? styles.shaUploadListItemError : ''}>
<div className={styles.shaUploadListItemInfo}>
{isUploading && <LoadingOutlined className={styles.shaUploadUploading} />}
<a target="_blank" title={file.name} onClick={onDownloadClick}>
{file.name} ({filesize(file.size)})
</a>

{!isUploading && fileControls()}
</div>
</div>
);
},
itemRender: (_originNode, file) => renderFileItem(file),
};

const showUploadButton = allowUpload && !fileInfo && !isUploading;
const classes = classNames(styles.shaUpload, { [styles.shaUploadHasFile]: fileInfo || isUploading });
const showUploadButton = allowUpload && !isUploading;
// const classes = classNames(styles.shaUpload, { [styles.shaUploadHasFile]: fileInfo || isUploading });

const uploadButton = (
<Button
icon={<UploadOutlined />}
icon={!fileInfo ? <UploadOutlined /> : <PictureOutlined />}
type="link"
ref={uploadButtonRef}
style={{ display: !showUploadButton ? 'none' : '' }}
>
(press to upload)
{listType === 'text' ? `(press to upload)` : null}
</Button>
);

const renderStub = () => {
if (isDragger) {
return <Dragger disabled><DraggerStub /></Dragger>;
return (
<Dragger disabled>
<DraggerStub styles={styles} />
</Dragger>
);
}

return <div className={classes}>{uploadButton}</div>;
return (
<>
<div
className={
listType === 'thumbnail' ? 'ant-upload-list-item-name ant-upload-list-item-name-stub thumbnail-stub' : ''
}
>
{uploadButton}
</div>
{listType === 'thumbnail' && !hideFileName ? <div className="thumbnail-item-name">File name</div> : null}
</>
);
};

const renderUploader = () => {
const antListType = listType === 'thumbnail' ? 'picture-card' : 'text';
if (isDragger && allowUpload) {
return (
<Dragger {...fileProps} className={classes}>
<Dragger {...fileProps}>
<span ref={uploadDraggerSpanRef} />
<DraggerStub />
<DraggerStub styles={styles} />
</Dragger>
);
}

return (
<Upload {...fileProps} listType='picture-card' className={classes}>
{/* {allowUpload && uploadButton} */}
</Upload>
<div>
<Upload {...fileProps} listType={antListType}>
{allowUpload && !fileInfo && uploadButton}
</Upload>
</div>
);
};


return <span className={styles.shaFileUploadContainer}>{isStub ? renderStub() : renderUploader()}</span>;
return (
<>
<span className={styles.shaStoredFilesRenderer}>{isStub ? renderStub() : renderUploader()}</span>
{previewOpen && (
<Image
wrapperStyle={{ display: 'none' }}
preview={{
visible: previewOpen,
onVisibleChange: (visible) => setPreviewOpen(visible),
toolbarRender: (original) => {
return (
<div style={{ display: 'flex', flexDirection: 'row-reverse' }}>
{hideFileName && (
<DownloadOutlined
className={styles.antPreviewDownloadIcon}
onClick={() => downloadFile({ fileId: previewImage?.uid, fileName: previewImage?.name })}
/>
)}
{original}
</div>
);
},
}}
src={imageUrl}
/>
)}
</>
);
Comment on lines +307 to +334
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Image preview implementation missing after-change handling.

The image preview implementation is good, but the afterOpenChange callback property is missing which was addressed in a previous review.

Image preview setup needs to handle state cleanup after the preview is closed:

<Image
  wrapperStyle={{ display: 'none' }}
  preview={{
    visible: previewOpen,
    onVisibleChange: (visible) => setPreviewOpen(visible),
+   afterOpenChange: (visible) => {
+     if (!visible) {
+       // Reset state if needed when preview is closed
+     }
+   },
    toolbarRender: (original) => {
      return (
        <div style={{ display: 'flex', flexDirection: 'row-reverse' }}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<>
<span className={styles.shaStoredFilesRenderer}>{isStub ? renderStub() : renderUploader()}</span>
{previewOpen && (
<Image
wrapperStyle={{ display: 'none' }}
preview={{
visible: previewOpen,
onVisibleChange: (visible) => setPreviewOpen(visible),
toolbarRender: (original) => {
return (
<div style={{ display: 'flex', flexDirection: 'row-reverse' }}>
{hideFileName && (
<DownloadOutlined
className={styles.antPreviewDownloadIcon}
onClick={() => downloadFile({ fileId: previewImage?.uid, fileName: previewImage?.name })}
/>
)}
{original}
</div>
);
},
}}
src={imageUrl}
/>
)}
</>
);
return (
<>
<span className={styles.shaStoredFilesRenderer}>
{isStub ? renderStub() : renderUploader()}
</span>
{previewOpen && (
<Image
wrapperStyle={{ display: 'none' }}
preview={{
visible: previewOpen,
onVisibleChange: (visible) => setPreviewOpen(visible),
afterOpenChange: (visible) => {
if (!visible) {
// Reset state if needed when preview is closed
}
},
toolbarRender: (original) => {
return (
<div style={{ display: 'flex', flexDirection: 'row-reverse' }}>
{hideFileName && (
<DownloadOutlined
className={styles.antPreviewDownloadIcon}
onClick={() =>
downloadFile({
fileId: previewImage?.uid,
fileName: previewImage?.name,
})
}
/>
)}
{original}
</div>
);
},
}}
src={imageUrl}
/>
)}
</>
);

};

export default FileUpload;
7 changes: 2 additions & 5 deletions shesha-reactjs/src/components/fileUpload/stubs.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import React, { FC } from 'react';
import React from 'react';
import { InboxOutlined } from '@ant-design/icons';
import { useStyles } from './styles/styles';

export const DraggerStub: FC = () => {
const { styles } = useStyles();

export const DraggerStub = ({styles}) => {
return (
<div>
<p className={styles.antUploadDragIcon}>
Expand Down
Loading
Loading