[FEAT] better management of upload files

This commit is contained in:
Edouard DUPIN 2024-09-20 22:19:10 +02:00
parent e9af64405b
commit 6107b95dcd
5 changed files with 148 additions and 63 deletions

View File

@ -38,10 +38,14 @@ export type PopUpUploadProgressProps = {
title: string; title: string;
// current size send or receive // current size send or receive
currentSize: number; currentSize: number;
// in case of error this element is set to != undefined
error?: string;
// Total size to transfer // Total size to transfer
totalSize: number; totalSize: number;
// index of the file to transfer // index of the file to transfer
index: number; index: number;
// When finished the boolean is set to true
isFinished: boolean;
// List of element to Transfer // List of element to Transfer
elements: string[]; elements: string[];
onAbort: () => void; onAbort: () => void;
@ -49,15 +53,16 @@ export type PopUpUploadProgressProps = {
}; };
export const PopUpUploadProgress = ({ export const PopUpUploadProgress = ({
title,
currentSize, currentSize,
totalSize,
index,
elements, elements,
error,
index,
isFinished,
onAbort, onAbort,
onClose, onClose,
title,
totalSize,
}: PopUpUploadProgressProps) => { }: PopUpUploadProgressProps) => {
const disclosure = useDisclosure();
const initialRef = useRef(null); const initialRef = useRef(null);
const finalRef = useRef(null); const finalRef = useRef(null);
return ( return (
@ -75,16 +80,46 @@ export const PopUpUploadProgress = ({
<ModalBody pb={6} paddingLeft="18px"> <ModalBody pb={6} paddingLeft="18px">
<Flex direction="column" gap="10px"> <Flex direction="column" gap="10px">
<Text fontSize="20px" fontWeight="bold">[{index}/{elements.length}] {elements[index]}</Text> {isFinished ? (
<Progress colorScheme='green' hasStripe value={currentSize} isAnimated max={totalSize} height='24px' /> <Text fontSize="20px" fontWeight="bold">
<Flex><Text>{currentSize.toLocaleString('fr-FR')} Bytes</Text><Text marginLeft="auto">{totalSize.toLocaleString('fr-FR')} Bytes</Text></Flex> All {elements.length} element have been sent
</Text>
) : (
<Text fontSize="20px" fontWeight="bold">
[{index + 1}/{elements.length}] {elements[index]}
</Text>
)}
<Progress
colorScheme="green"
hasStripe
value={currentSize}
isAnimated
max={totalSize}
height="24px"
/>
<Flex>
<Text>{currentSize.toLocaleString('fr-FR')} Bytes</Text>
<Text marginLeft="auto">
{totalSize.toLocaleString('fr-FR')} Bytes
</Text>
</Flex>
{error && (
<Text fontWeight="bold" color="darkred">
{error}
</Text>
)}
</Flex> </Flex>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button colorScheme="red" mr={3} onClick={onAbort} ref={initialRef}> {isFinished ? (
Abort <Button onClick={onClose} variant="@success">
</Button> Ok
<Button onClick={onClose}>Close</Button> </Button>
) : (
<Button colorScheme="red" mr={3} onClick={onAbort} ref={initialRef}>
Abort
</Button>
)}
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@ -1,22 +1,20 @@
import { useState } from 'react'; import { useCallback, useState } from 'react';
import { import {
Button, Button,
Flex, Flex,
Input, Input,
Table, Table,
TableCaption,
TableContainer, TableContainer,
Tbody, Tbody,
Td, Td,
Text, Text,
Tfoot,
Th, Th,
Thead, Thead,
Tr, Tr,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { LuTrash } from 'react-icons/lu'; import { LuTrash } from 'react-icons/lu';
import { MdCloudUpload, MdRemove } from 'react-icons/md'; import { MdCloudUpload } from 'react-icons/md';
import { import {
Album, Album,
@ -31,7 +29,6 @@ import {
} from '@/back-api'; } from '@/back-api';
import { PageLayout } from '@/components/Layout/PageLayout'; import { PageLayout } from '@/components/Layout/PageLayout';
import { TopBar } from '@/components/TopBar/TopBar'; import { TopBar } from '@/components/TopBar/TopBar';
import { FormInput } from '@/components/form/FormInput';
import { FormSelect } from '@/components/form/FormSelect'; import { FormSelect } from '@/components/form/FormSelect';
import { useFormidable } from '@/components/form/Formidable'; import { useFormidable } from '@/components/form/Formidable';
import { PopUpUploadProgress } from '@/components/popup/PopUpUploadProgress'; import { PopUpUploadProgress } from '@/components/popup/PopUpUploadProgress';
@ -39,6 +36,7 @@ import { useAlbumService, useOrderedAlbums } from '@/service/Album';
import { useArtistService, useOrderedArtists } from '@/service/Artist'; import { useArtistService, useOrderedArtists } from '@/service/Artist';
import { useGenderService, useOrderedGenders } from '@/service/Gender'; import { useGenderService, useOrderedGenders } from '@/service/Gender';
import { useServiceContext } from '@/service/ServiceContext'; import { useServiceContext } from '@/service/ServiceContext';
import { useTrackService } from '@/service/Track';
import { isNullOrUndefined } from '@/utils/validator'; import { isNullOrUndefined } from '@/utils/validator';
export class ElementList { export class ElementList {
@ -54,21 +52,24 @@ export class FileParsedElement {
public nameDetected: boolean = false; public nameDetected: boolean = false;
public trackIdDetected: boolean = false; public trackIdDetected: boolean = false;
constructor( constructor(
public uniqueId: number,
public file: File, public file: File,
public title: string, public title: string,
public artist?: string, public artist?: string,
public album?: string, public album?: string,
public trackId?: number public trackId?: number
) { ) {
console.log(`Unique element: ${uniqueId}`);
// nothing to do. // nothing to do.
} }
} }
export class FileFailParsedElement { export class FileFailParsedElement {
constructor( constructor(
public id: number, public uniqueId: number,
public file: File, public file: File,
public reason: string public reason: string
) { ) {
console.log(`Unique element2: ${uniqueId}`);
// nothing to do. // nothing to do.
} }
} }
@ -98,6 +99,7 @@ export const AddPage = () => {
const { store: storeGender } = useGenderService(); const { store: storeGender } = useGenderService();
const { store: storeArtist } = useArtistService(); const { store: storeArtist } = useArtistService();
const { store: storeAlbum } = useAlbumService(); const { store: storeAlbum } = useAlbumService();
const { store: storeTrack } = useTrackService();
const { session } = useServiceContext(); const { session } = useServiceContext();
const form = useFormidable<FormInsertData>({}); const form = useFormidable<FormInsertData>({});
@ -160,7 +162,7 @@ export const AddPage = () => {
setSuggestedAlbum(undefined); setSuggestedAlbum(undefined);
}; };
const addFileWithMetaData = (file: File) => { const addFileWithMetaData = (file: File, id: number) => {
// parsedElement: FileParsedElement[] = []; // parsedElement: FileParsedElement[] = [];
let artist: string | undefined = undefined; let artist: string | undefined = undefined;
let album: string | undefined = undefined; let album: string | undefined = undefined;
@ -199,7 +201,14 @@ export const AddPage = () => {
} }
// remove extension // remove extension
title = title.replace(new RegExp('\\.(webm|WEBM|Webm)'), ''); title = title.replace(new RegExp('\\.(webm|WEBM|Webm)'), '');
let tmp = new FileParsedElement(file, title, artist, album, trackIdNumber); let tmp = new FileParsedElement(
id,
file,
title,
artist,
album,
trackIdNumber
);
console.log(`==>${JSON.stringify(tmp)}`); console.log(`==>${JSON.stringify(tmp)}`);
// add it in the list. // add it in the list.
return tmp; return tmp;
@ -218,7 +227,7 @@ export const AddPage = () => {
const parsedFailedElementTmp: FileFailParsedElement[] = []; const parsedFailedElementTmp: FileFailParsedElement[] = [];
for (let iii = 0; iii < value.target.files?.length; iii++) { for (let iii = 0; iii < value.target.files?.length; iii++) {
parsedElementTmp.push(addFileWithMetaData(value.target.files[iii])); parsedElementTmp.push(addFileWithMetaData(value.target.files[iii], iii));
} }
// check if all global parameters are generic: // check if all global parameters are generic:
if (parsedElementTmp.length === 0) { if (parsedElementTmp.length === 0) {
@ -311,51 +320,73 @@ export const AddPage = () => {
const [listValues, setListValues] = useState<string[]>([]); const [listValues, setListValues] = useState<string[]>([]);
const [currentPosition, setCurrentPosition] = useState<number>(0); const [currentPosition, setCurrentPosition] = useState<number>(0);
const [totalPosition, setTotalPosition] = useState<number>(0); const [totalPosition, setTotalPosition] = useState<number>(0);
const [uploadError, setUploadError] = useState<string | undefined>(undefined);
const [isFinishedUpload, setIsFinishedUpload] = useState<boolean>(false);
function progressUpload(count: number, total: number): void { const progressUpload = useCallback(
setTotalPosition(total); (count: number, total: number) => {
setCurrentPosition(count); setTotalPosition(total);
} setCurrentPosition(count);
},
[setTotalPosition, setCurrentPosition]
);
const uploadNext = (): void => { const uploadNext = useCallback(
setIndexUpload((previous) => previous ?? -1 + 1); (index: number = 0): void => {
if (parsedElement.length <= index) {
TrackResource.uploadTrack({ console.log('end of upload');
restConfig: session.getRestConfig(), setIsFinishedUpload(true);
data: { return;
title: parsedElement[0].title, }
file: parsedElement[0].file, setIndexUpload(index);
albumId: form.values['albumId'] ?? undefined, console.log(
artistId: form.values['artistId'] ?? undefined, `Start upload of file: ${index}: ${parsedElement[index].title}`
genderId: form.values['genderId'] ?? undefined, );
trackId: parsedElement[0].trackId ?? undefined, storeTrack
}, .update(
callbacks: { TrackResource.uploadTrack({
progressUpload: progressUpload, restConfig: session.getRestConfig(),
}, data: {
}) title: parsedElement[index].title,
.then((data: Track) => { file: parsedElement[index].file,
// TODO: add the element in the local base... albumId: form.values['albumId'] ?? undefined,
console.log(`element added: ${JSON.stringify(data, null, 2)}`); artistId: form.values['artistId'] ?? undefined,
}) genderId: form.values['genderId'] ?? undefined,
.catch((error: RestErrorResponse) => { trackId: parsedElement[index].trackId ?? undefined,
// TODO: manage error },
console.log(`element error: ${JSON.stringify(error, null, 2)}`); callbacks: {
}); progressUpload: progressUpload,
}; },
})
)
.then((data: Track) => {
// element sended good
// Send next ...
uploadNext(index + 1);
})
.catch((error: RestErrorResponse) => {
// TODO: manage error
console.log(`element error: ${JSON.stringify(error, null, 2)}`);
setUploadError(JSON.stringify(error, null, 2));
});
},
[setUploadError, setIndexUpload, storeTrack, parsedElement]
);
const sendFile = (): void => { const sendFile = (): void => {
console.log(`Send file requested ... ${parsedElement.length}`); console.log(`Send file requested ... ${parsedElement.length}`);
setUploadError(undefined);
setIsFinishedUpload(false);
setListValues(parsedElement.map((element) => element.file.name)); setListValues(parsedElement.map((element) => element.file.name));
uploadNext(); uploadNext();
}; };
function onUploadAbort(): void { function onUploadAbort(): void {
//throw new Error('Function not implemented.'); setIndexUpload(undefined);
} }
function OnUploadClose(): void { function OnUploadClose(): void {
//throw new Error('Function not implemented.'); setIndexUpload(undefined);
} }
const addNewGender = (data: string): Promise<Gender> => { const addNewGender = (data: string): Promise<Gender> => {
@ -467,7 +498,7 @@ export const AddPage = () => {
</Thead> </Thead>
<Tbody> <Tbody>
{parsedElement.map((data) => ( {parsedElement.map((data) => (
<Tr key={data.file.name}> <Tr key={data.uniqueId}>
<Td> <Td>
<Input <Input
type="number" type="number"
@ -591,7 +622,7 @@ export const AddPage = () => {
</Thead> </Thead>
<Tbody> <Tbody>
{parsedFailedElement.map((data) => ( {parsedFailedElement.map((data) => (
<Tr key={data.file.name}> <Tr key={data.uniqueId}>
<Td>{data.file.name}</Td> <Td>{data.file.name}</Td>
<Td>{data.reason}</Td> <Td>{data.reason}</Td>
</Tr> </Tr>
@ -609,8 +640,10 @@ export const AddPage = () => {
totalSize={totalPosition} totalSize={totalPosition}
index={indexUpload} index={indexUpload}
elements={listValues} elements={listValues}
error={uploadError}
onAbort={onUploadAbort} onAbort={onUploadAbort}
onClose={OnUploadClose} onClose={OnUploadClose}
isFinished={isFinishedUpload}
/> />
)} )}
</PageLayout> </PageLayout>

View File

@ -25,7 +25,7 @@ export const SSOPage = () => {
clearToken(); clearToken();
} }
}, [token, setToken, clearToken]); }, [token, setToken, clearToken]);
const delay = isDevelopmentEnvironment() ? 6000 : 2000; const delay = isDevelopmentEnvironment() ? 2000 : 2000;
useEffect(() => { useEffect(() => {
if (state === SessionState.CONNECTED) { if (state === SessionState.CONNECTED) {
const destination = data ? b64_to_utf8(data) : '/'; const destination = data ? b64_to_utf8(data) : '/';

View File

@ -76,6 +76,15 @@ export default defineStyleConfig({
color: mode('white', 'error.900')(props), color: mode('white', 'error.900')(props),
boxColorFocus: mode('error.900', 'error.500')(props), boxColorFocus: mode('error.900', 'error.500')(props),
}), }),
'@success': (props) =>
customVariant({
theme: props.theme,
bg: mode('green.300', 'green.300')(props),
bgHover: mode('green.400', 'green.400')(props),
bgActive: mode('green.500', 'green.400')(props),
color: mode('white', 'green.900')(props),
boxColorFocus: mode('green.900', 'green.400')(props),
}),
'@progress': (props) => ({ '@progress': (props) => ({
...customVariant({ ...customVariant({

View File

@ -16,6 +16,7 @@ export type DataStoreType<TYPE> = {
get: <MODEL>(value: MODEL, key?: string) => TYPE | undefined; get: <MODEL>(value: MODEL, key?: string) => TYPE | undefined;
gets: <MODEL>(value: MODEL[] | undefined, key?: string) => TYPE[]; gets: <MODEL>(value: MODEL[] | undefined, key?: string) => TYPE[];
update: (request: Promise<TYPE>, key?: string) => Promise<TYPE>; update: (request: Promise<TYPE>, key?: string) => Promise<TYPE>;
updateRaw: (data: TYPE, key?: string) => void;
remove: (id: number | string, request: Promise<void>, key?: string) => void; remove: (id: number | string, request: Promise<void>, key?: string) => void;
}; };
@ -95,17 +96,24 @@ export const useDataStore = <TYPE>(
[data] [data]
); );
const updateRaw = useCallback(
(responseData: TYPE, key?: string): void => {
const keyValue = key ?? primaryKey;
const filterData = data.filter(
(localData: TYPE) => localData[keyValue] !== responseData[keyValue]
);
filterData.push(responseData);
setData(filterData);
},
[data, setData]
);
const update = useCallback( const update = useCallback(
(request: Promise<TYPE>, key?: string): Promise<TYPE> => { (request: Promise<TYPE>, key?: string): Promise<TYPE> => {
const keyValue = key ?? primaryKey;
return new Promise((resolve, rejects) => { return new Promise((resolve, rejects) => {
request request
.then((responseData: TYPE) => { .then((responseData: TYPE) => {
const filterData = data.filter( updateRaw(responseData, key);
(localData: TYPE) => localData[keyValue] !== responseData[keyValue]
);
filterData.push(responseData);
setData(filterData);
resolve(responseData); resolve(responseData);
}) })
.catch((error: RestErrorResponse) => { .catch((error: RestErrorResponse) => {
@ -114,7 +122,7 @@ export const useDataStore = <TYPE>(
}); });
}); });
}, },
[data, setData] [data, updateRaw]
); );
const remove = useCallback( const remove = useCallback(
(id: number | string, request: Promise<void>, key?: string) => { (id: number | string, request: Promise<void>, key?: string) => {
@ -136,5 +144,5 @@ export const useDataStore = <TYPE>(
[data, setData] [data, setData]
); );
return { isLoading, error, data, get, gets, update, remove }; return { isLoading, error, data, get, gets, update, updateRaw, remove };
}; };