Merge branch 'fe/develop' of https://lab.ssafy.com/s11-s-project/S11P21S002 into fe/refactor/review-detail
This commit is contained in:
commit
7381b67cb6
@ -56,7 +56,7 @@ export async function uploadImageFile(
|
|||||||
.then(({ data }) => data);
|
.then(({ data }) => data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadImageFolder(
|
export async function uploadImageFolderFile(
|
||||||
memberId: number,
|
memberId: number,
|
||||||
projectId: number,
|
projectId: number,
|
||||||
folderId: number,
|
folderId: number,
|
||||||
@ -81,6 +81,31 @@ export async function uploadImageFolder(
|
|||||||
.then(({ data }) => data);
|
.then(({ data }) => data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadImageFolder(
|
||||||
|
memberId: number,
|
||||||
|
projectId: number,
|
||||||
|
folderId: number,
|
||||||
|
files: File[],
|
||||||
|
processCallback: (progress: number) => void
|
||||||
|
) {
|
||||||
|
const formData = new FormData();
|
||||||
|
files.forEach((file) => {
|
||||||
|
formData.append('imageList', file);
|
||||||
|
});
|
||||||
|
|
||||||
|
return api
|
||||||
|
.post(`/projects/${projectId}/folders/${folderId}/images/folder`, formData, {
|
||||||
|
params: { memberId },
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
if (progressEvent.total) {
|
||||||
|
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||||
|
processCallback(progress);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(({ data }) => data);
|
||||||
|
}
|
||||||
|
|
||||||
export async function uploadImageZip(
|
export async function uploadImageZip(
|
||||||
memberId: number,
|
memberId: number,
|
||||||
projectId: number,
|
projectId: number,
|
||||||
|
@ -0,0 +1,190 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import useAuthStore from '@/stores/useAuthStore';
|
||||||
|
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
|
||||||
|
import useUploadImageFolderFileQuery from '@/queries/projects/useUploadImageFolderFileQuery';
|
||||||
|
|
||||||
|
export default function ImageUploadFolderFileForm({
|
||||||
|
onClose,
|
||||||
|
onRefetch,
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
onRefetch?: () => void;
|
||||||
|
projectId: number;
|
||||||
|
folderId: number;
|
||||||
|
}) {
|
||||||
|
const profile = useAuthStore((state) => state.profile);
|
||||||
|
const memberId = profile?.id || 0;
|
||||||
|
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
const [inputKey, setInputKey] = useState<number>(0);
|
||||||
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||||
|
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||||
|
const [isUploaded, setIsUploaded] = useState<boolean>(false);
|
||||||
|
const [isFailed, setIsFailed] = useState<boolean>(false);
|
||||||
|
const [progress, setProgress] = useState<number>(0);
|
||||||
|
|
||||||
|
const uploadImageFolderFile = useUploadImageFolderFileQuery();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefetch = () => {
|
||||||
|
if (onRefetch) {
|
||||||
|
onRefetch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newFiles = event.target.files;
|
||||||
|
|
||||||
|
if (newFiles) {
|
||||||
|
setFiles((prevFiles) => [...prevFiles, ...Array.from(newFiles)]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFile = () => {
|
||||||
|
setFiles([]);
|
||||||
|
setInputKey((prevKey) => prevKey + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async () => {
|
||||||
|
setIsUploading(true);
|
||||||
|
|
||||||
|
uploadImageFolderFile.mutate(
|
||||||
|
{
|
||||||
|
memberId,
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
files,
|
||||||
|
progressCallback: (progress: number) => {
|
||||||
|
setProgress(progress);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleRefetch();
|
||||||
|
setIsUploaded(true);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setIsFailed(true);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
{!isUploading && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex h-[200px] w-full cursor-pointer items-center justify-center rounded-lg border-2 text-center',
|
||||||
|
isDragging ? 'border-solid border-primary bg-blue-200' : 'border-dashed border-gray-500 bg-gray-100'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
key={inputKey}
|
||||||
|
type="file"
|
||||||
|
webkitdirectory=""
|
||||||
|
// multiple
|
||||||
|
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
||||||
|
onChange={handleChange}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
/>
|
||||||
|
{isDragging ? (
|
||||||
|
<p className="text-primary">드래그한 폴더를 여기에 놓으세요</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">
|
||||||
|
폴더를 업로드하려면 여기를 클릭하거나
|
||||||
|
<br />
|
||||||
|
폴더를 드래그하여 여기에 놓으세요
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className={'flex items-center justify-between p-1'}>
|
||||||
|
<span className="truncate">
|
||||||
|
{files[0].webkitRelativePath.substring(0, files[0].webkitRelativePath.indexOf('/'))} {}
|
||||||
|
</span>
|
||||||
|
{isUploading ? (
|
||||||
|
<div className="p-2">
|
||||||
|
{isUploaded ? (
|
||||||
|
<CircleCheckBig
|
||||||
|
className="stroke-green-500"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
) : isFailed ? (
|
||||||
|
<CircleX
|
||||||
|
className="stroke-red-500"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CircleDashed
|
||||||
|
className="stroke-gray-500"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className={'cursor-pointer p-2'}
|
||||||
|
onClick={handleRemoveFile}
|
||||||
|
>
|
||||||
|
<X
|
||||||
|
color="red"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isUploading ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleClose}
|
||||||
|
variant="outlinePrimary"
|
||||||
|
className={
|
||||||
|
isFailed
|
||||||
|
? 'border-red-500 text-red-500 hover:bg-red-500 dark:border-red-500 dark:text-red-500 dark:hover:bg-red-500'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
disabled={!isUploaded && !isFailed}
|
||||||
|
>
|
||||||
|
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : `업로드 중... ${progress}%`}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={handleUpload}
|
||||||
|
variant="outlinePrimary"
|
||||||
|
disabled={files.length === 0}
|
||||||
|
>
|
||||||
|
업로드
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
35
frontend/src/components/ImageUploadFolderFileModal/index.tsx
Normal file
35
frontend/src/components/ImageUploadFolderFileModal/index.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import ImageUploadFolderFileForm from './ImageUploadFolderFileForm';
|
||||||
|
|
||||||
|
export default function ImageUploadFolderFileModal({ projectId, folderId }: { projectId: number; folderId: number }) {
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const handleOpen = () => setIsOpen(true);
|
||||||
|
const handleClose = () => setIsOpen(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={setIsOpen}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center p-2"
|
||||||
|
onClick={handleOpen}
|
||||||
|
>
|
||||||
|
<Plus size={20} />
|
||||||
|
</button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader title="폴더 업로드 (파일 업로드 API 이용)" />
|
||||||
|
<ImageUploadFolderFileForm
|
||||||
|
onClose={handleClose}
|
||||||
|
projectId={projectId}
|
||||||
|
folderId={folderId}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@ -20,6 +20,7 @@ export default function ImageUploadFolderForm({
|
|||||||
const memberId = profile?.id || 0;
|
const memberId = profile?.id || 0;
|
||||||
|
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
const [inputKey, setInputKey] = useState<number>(0);
|
||||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||||
const [isUploading, setIsUploading] = useState<boolean>(false);
|
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||||
const [isUploaded, setIsUploaded] = useState<boolean>(false);
|
const [isUploaded, setIsUploaded] = useState<boolean>(false);
|
||||||
@ -44,8 +45,6 @@ export default function ImageUploadFolderForm({
|
|||||||
if (newFiles) {
|
if (newFiles) {
|
||||||
setFiles((prevFiles) => [...prevFiles, ...Array.from(newFiles)]);
|
setFiles((prevFiles) => [...prevFiles, ...Array.from(newFiles)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
event.target.value = '';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
@ -62,8 +61,9 @@ export default function ImageUploadFolderForm({
|
|||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFile = (index: number) => {
|
const handleRemoveFile = () => {
|
||||||
setFiles(files.filter((_, i) => i != index));
|
setFiles([]);
|
||||||
|
setInputKey((prevKey) => prevKey + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
@ -101,6 +101,7 @@ export default function ImageUploadFolderForm({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
key={inputKey}
|
||||||
type="file"
|
type="file"
|
||||||
webkitdirectory=""
|
webkitdirectory=""
|
||||||
// multiple
|
// multiple
|
||||||
@ -122,13 +123,10 @@ export default function ImageUploadFolderForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<ul className="m-0 max-h-[260px] list-none overflow-y-auto p-0">
|
<div className={'flex items-center justify-between p-1'}>
|
||||||
{files.map((file, index) => (
|
<span className="truncate">
|
||||||
<li
|
{files[0].webkitRelativePath.substring(0, files[0].webkitRelativePath.indexOf('/'))} {}
|
||||||
key={index}
|
</span>
|
||||||
className="flex items-center justify-between p-1"
|
|
||||||
>
|
|
||||||
<span className="truncate">{file.webkitRelativePath || file.name}</span>
|
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{isUploaded ? (
|
{isUploaded ? (
|
||||||
@ -154,7 +152,7 @@ export default function ImageUploadFolderForm({
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className={'cursor-pointer p-2'}
|
className={'cursor-pointer p-2'}
|
||||||
onClick={() => handleRemoveFile(index)}
|
onClick={handleRemoveFile}
|
||||||
>
|
>
|
||||||
<X
|
<X
|
||||||
color="red"
|
color="red"
|
||||||
@ -163,9 +161,7 @@ export default function ImageUploadFolderForm({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</li>
|
</div>
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
)}
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<Button
|
<Button
|
||||||
|
@ -23,7 +23,7 @@ export default function ImageUploadFolderModal({ projectId, folderId }: { projec
|
|||||||
</button>
|
</button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader title="폴더 업로드" />
|
<DialogHeader title="폴더 업로드 (백엔드 구현 필요)" />
|
||||||
<ImageUploadFolderForm
|
<ImageUploadFolderForm
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||||
import ImageUploadFileForm from '../ImageUploadFileModal/ImageUploadFileForm';
|
import ImageUploadFileForm from '../ImageUploadFileModal/ImageUploadFileForm';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import ImageUploadFolderFileForm from '../ImageUploadFolderFileModal/ImageUploadFolderFileForm';
|
||||||
import ImageUploadFolderForm from '../ImageUploadFolderModal/ImageUploadFolderForm';
|
import ImageUploadFolderForm from '../ImageUploadFolderModal/ImageUploadFolderForm';
|
||||||
import ImageUploadZipForm from '../ImageUploadZipModal/ImageUploadZipForm';
|
import ImageUploadZipForm from '../ImageUploadZipModal/ImageUploadZipForm';
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ export default function WorkspaceDropdownMenu({
|
|||||||
}) {
|
}) {
|
||||||
const [isOpenUploadFile, setIsOpenUploadFile] = React.useState<boolean>(false);
|
const [isOpenUploadFile, setIsOpenUploadFile] = React.useState<boolean>(false);
|
||||||
const [fileCount, setFileCount] = React.useState<number>(0);
|
const [fileCount, setFileCount] = React.useState<number>(0);
|
||||||
|
const [isOpenUploadFolderFile, setIsOpenUploadFolderFile] = React.useState<boolean>(false);
|
||||||
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
|
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
|
||||||
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
|
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
|
||||||
|
|
||||||
@ -32,6 +34,12 @@ export default function WorkspaceDropdownMenu({
|
|||||||
setIsOpenUploadFile(false);
|
setIsOpenUploadFile(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenUploadFolderFile = () => setIsOpenUploadFolderFile(true);
|
||||||
|
|
||||||
|
const handleCloseUploadFolderFile = () => {
|
||||||
|
setIsOpenUploadFolderFile(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenUploadFolder = () => setIsOpenUploadFolder(true);
|
const handleOpenUploadFolder = () => setIsOpenUploadFolder(true);
|
||||||
|
|
||||||
const handleCloseUploadFolder = () => {
|
const handleCloseUploadFolder = () => {
|
||||||
@ -45,7 +53,6 @@ export default function WorkspaceDropdownMenu({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFileCount = (fileCount: number) => {
|
const handleFileCount = (fileCount: number) => {
|
||||||
console.log(fileCount);
|
|
||||||
setFileCount(fileCount);
|
setFileCount(fileCount);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,7 +75,8 @@ export default function WorkspaceDropdownMenu({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={handleOpenUploadFile}>파일 업로드</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleOpenUploadFile}>파일 업로드</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={handleOpenUploadFolder}>폴더 업로드 (임시)</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleOpenUploadFolderFile}>폴더 업로드 (파일 업로드 API 이용)</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleOpenUploadFolder}>폴더 업로드 (백엔드 구현 필요)</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={handleOpenUploadZip}>폴더 압축파일 업로드</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleOpenUploadZip}>폴더 압축파일 업로드</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@ -90,13 +98,29 @@ export default function WorkspaceDropdownMenu({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isOpenUploadFolderFile}
|
||||||
|
onOpenChange={setIsOpenUploadFolderFile}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild></DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader title="폴더 업로드 (파일 업로드 API 이용)" />
|
||||||
|
<ImageUploadFolderFileForm
|
||||||
|
onClose={handleCloseUploadFolderFile}
|
||||||
|
onRefetch={onRefetch}
|
||||||
|
projectId={projectId}
|
||||||
|
folderId={folderId}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isOpenUploadFolder}
|
open={isOpenUploadFolder}
|
||||||
onOpenChange={setIsOpenUploadFolder}
|
onOpenChange={setIsOpenUploadFolder}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild></DialogTrigger>
|
<DialogTrigger asChild></DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader title="폴더 업로드 (임시)" />
|
<DialogHeader title="폴더 업로드 (백엔드 구현 필요)" />
|
||||||
<ImageUploadFolderForm
|
<ImageUploadFolderForm
|
||||||
onClose={handleCloseUploadFolder}
|
onClose={handleCloseUploadFolder}
|
||||||
onRefetch={onRefetch}
|
onRefetch={onRefetch}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import { uploadImageFolderFile } from '@/api/imageApi';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export default function useUploadImageFolderFileQuery() {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
memberId,
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
files,
|
||||||
|
progressCallback,
|
||||||
|
}: {
|
||||||
|
memberId: number;
|
||||||
|
projectId: number;
|
||||||
|
folderId: number;
|
||||||
|
files: File[];
|
||||||
|
progressCallback: (progress: number) => void;
|
||||||
|
}) => uploadImageFolderFile(memberId, projectId, folderId, files, progressCallback),
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user