Skip to content

Commit

Permalink
feat: implement file upload and download
Browse files Browse the repository at this point in the history
Some issues related to file download are left
  • Loading branch information
swouf committed Apr 3, 2023
1 parent ed9def3 commit 33c310a
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 83 deletions.
113 changes: 41 additions & 72 deletions src/components/ExcalidrawView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ import Box from '@mui/material/Box';
import { Excalidraw } from '@excalidraw/excalidraw';
import type {
ExcalidrawElement,
FileId,
ExcalidrawImageElement,
} from '@excalidraw/excalidraw/types/element/types';
import {
AppState,
BinaryFileData,
BinaryFiles,
DataURL,
ExcalidrawImperativeAPI,
} from '@excalidraw/excalidraw/types/types';

Expand All @@ -39,6 +37,7 @@ import {
EXCALIDRAW_ENABLE_ZEN_MODE,
EXCALIDRAW_THEME,
} from '../config/settings';
import { getListOfFileIds, useFiles } from '../data/files';
import { reconcileElements } from '../utils/reconciliation';
import Loader from './common/Loader';
import RefreshButton from './common/RefreshButton';
Expand All @@ -50,63 +49,26 @@ const ExcalidrawView: FC = () => {
const date = new Date();
const name = `drawing${date.toISOString()}`;

const [localElements, setLocalElements] =
useState<readonly ExcalidrawElement[]>();
const [localElements, setLocalElements] = useState<
readonly ExcalidrawElement[]
>([]);
const [idElements, setIdElements] = useState<string>('');
const [idState, setIdState] = useState<string>('');
const [appState, setAppState] = useState<AppState>();
const [filesAppData, setFilesAppData] = useState<List<FileAppData>>(List());
const files = useFiles(filesAppData);

const {
appDataArray,
patchAppData,
postAppData,
deleteAppData,
uploadFile,
getFileContent,
deleteFile,
status,
} = useAppDataContext();

const { isLoading } = status;

const getFile = useCallback(
async (fileAppData: FileAppData): Promise<BinaryFileData> => {
const { id, data } = fileAppData;
const { name: fileName, mimetype } = data.extra.s3File;
const fileData = await getFileContent(id || '').then((result) => {
console.log('fileData', result);
return result;
});
const file: BinaryFileData = {
mimeType: mimetype as BinaryFileData['mimeType'],
id: fileName as FileId,
dataURL: URL.createObjectURL(fileData) as DataURL,
created: data?.created as number,
};

return file;
},
[getFileContent],
);

const addAllFilesToExcalidraw = useCallback(
async (filesAppDataToAdd: List<FileAppData>): Promise<void> => {
const promisedFiles = filesAppDataToAdd.map(getFile);
const files: BinaryFileData[] = [];
promisedFiles.map((promisedFile) =>
promisedFile.then((d) => files.push(d)),
);
console.log('Files to be added: ', files);
excalidrawRef.current?.addFiles(files);
},
[getFile],
);

useEffect(() => {
// Add all the files to excalidraw
addAllFilesToExcalidraw(filesAppData);
}, [addAllFilesToExcalidraw, filesAppData]);

useEffect(() => {
const elementsAppData: ExcalidrawElementsAppData = (appDataArray.find(
({ type }) => type === APP_DATA_TYPES.EXCALIDRAW_ELEMENTS,
Expand Down Expand Up @@ -143,7 +105,7 @@ const ExcalidrawView: FC = () => {
elements: reconciledElements,
commitToHistory: false,
});
setLocalElements(reconciledElements);
setLocalElements(reconciledElements as readonly ExcalidrawElement[]);
} else {
setLocalElements(newElements);
}
Expand All @@ -155,6 +117,10 @@ const ExcalidrawView: FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appDataArray]);

useEffect(() => {
excalidrawRef.current?.addFiles(files);
}, [files]);

const saveElements = (
elements: readonly ExcalidrawElement[],
id: string,
Expand Down Expand Up @@ -210,31 +176,31 @@ const ExcalidrawView: FC = () => {

const compareAndSaveFiles = useCallback(
async (
files: BinaryFiles,
currentFileData: List<FileAppData>,
filesLocal: BinaryFiles,
filesAppDataCurrent: List<FileAppData>,
elements: ExcalidrawElement[],
): Promise<void> => {
const listOfFiles = List(Object.values(files));
console.log('Handling these files: ', listOfFiles);
console.log('With already these files: ', currentFileData);

const listOfIds = listOfFiles.map(({ id }) => String(id));
currentFileData
.filterNot(({ data: fileData }) => listOfIds.includes(fileData.name))
.forEach(({ id }) => deleteAppData({ id }));
if (!listOfFiles.isEmpty()) {
const listOfNames = currentFileData.map(
({ data: fileData }) => fileData.name,
);
listOfFiles
.filterNot(({ id }) => listOfNames.includes(id))
.forEach((file) => {
if (file.id) {
uploadFile(file);
}
});
}
const listOfIds = getListOfFileIds(
elements.filter(
(element) =>
Object.keys(element).includes('fileId') &&
element?.isDeleted === false,
) as ExcalidrawImageElement[],
);
const listOfUploadedIds = filesAppDataCurrent.map(
({ data }) => data.name,
);
Object.values(filesLocal).forEach((file) => {
const isUsed = listOfIds.includes(file.id);
const isUploaded = listOfUploadedIds.includes(file.id);
if (isUsed && !isUploaded) {
uploadFile(file);
} else if (!isUsed && isUploaded) {
deleteFile(file.id);
}
});
},
[deleteAppData, uploadFile],
[deleteFile, uploadFile],
);

const debouncedCompareElements = useRef(
Expand All @@ -252,7 +218,7 @@ const ExcalidrawView: FC = () => {
const handleChange = (
elements: readonly ExcalidrawElement[],
newAppState: AppState,
files: BinaryFiles,
filesLocal: BinaryFiles,
): void => {
const {
isResizing,
Expand All @@ -264,12 +230,15 @@ const ExcalidrawView: FC = () => {
typeof localElements !== 'undefined'
) {
debouncedCompareElements(elements, localElements, idElements);
debouncedCompareAndSaveFiles(files, filesAppData);
if (!appState?.pendingImageElementId) {
debouncedCompareAndSaveFiles(filesLocal, filesAppData, [...elements]);
}
debouncedSaveState(newAppState, idState);
} else {
debouncedCompareElements.cancel();
debouncedSaveElements.cancel();
debouncedSaveState.cancel();
debouncedCompareAndSaveFiles.cancel();
}
};

Expand All @@ -290,7 +259,7 @@ const ExcalidrawView: FC = () => {
initialData={{
elements: localElements,
appState,
files: filesAppData.map(getFile).toArray(),
files,
}}
ref={excalidrawRef}
onChange={handleChange}
Expand Down
39 changes: 28 additions & 11 deletions src/components/context/AppDataContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export type AppDataContextType = {
patchAppData: (payload: PatchAppDataType) => void;
deleteAppData: (payload: DeleteAppDataType) => void;
uploadFile: (fileToUpload: FileToUploadType) => Promise<void>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getFileContent: (fileId: string) => Promise<any>;
deleteFile: (fileId: string) => Promise<void>;
appDataArray: List<AppData>;
status: { isLoading: boolean; isFetching: boolean; isPreviousData: boolean };
};
Expand All @@ -69,8 +71,10 @@ const defaultContextValue = {
deleteAppData: () => null,
// eslint-disable-next-line @typescript-eslint/no-empty-function
uploadFile: () => new Promise<void>(() => {}), // Maybe there is a better way to write this?
// eslint-disable-next-line @typescript-eslint/no-empty-function
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-explicit-any
getFileContent: () => new Promise<any>(() => {}),
// eslint-disable-next-line @typescript-eslint/no-empty-function
deleteFile: () => new Promise<void>(() => {}),
appDataArray: List<AppData>(),
status: {
isFetching: false,
Expand Down Expand Up @@ -109,7 +113,8 @@ export const AppDataProvider: FC<PropsWithChildren> = ({ children }) => {

const uploadFile = useCallback(
async (fileToUpload: FileToUploadType): Promise<void> => {
const { mimeType, id, dataURL } = fileToUpload;
// console.log('Uploading: ', fileToUpload);
const { mimeType, id, dataURL, created } = fileToUpload;
const xhr = new XMLHttpRequest();
xhr.open(
'POST',
Expand All @@ -123,32 +128,42 @@ export const AppDataProvider: FC<PropsWithChildren> = ({ children }) => {
formData.append(id, file, id);
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(`File ${id} of type ${mimeType} successfully sent!`);
// console.log(`File ${id} of type ${mimeType} successfully sent!`);
onFileUploadComplete({
id: itemId,
data: xhr.response?.body?.[0].filter(Boolean),
data: { ...xhr.response?.body?.[0].filter(Boolean), created },
});
}
};
console.log(`Sending ${id} of type ${mimeType}...`);
// console.log(`Sending ${id} of type ${mimeType}...`);
xhr.send(formData);
},
[itemId, onFileUploadComplete, token],
);

const getFileContent = useCallback(
(fileId: string): Promise<any> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async (fileId: string): Promise<any> => {
const content = Api.getFileContent({
id: fileId,
apiHost: REACT_APP_API_HOST,
token: token || '',
}).then((contentData) => contentData);
console.log('File content: ', content);
return content;
},
[token],
);

const deleteFile = useCallback(
async (fileId: string) => {
const fileAppData = data?.find(({ data: file }) => file?.name === fileId);
if (fileAppData) {
deleteAppData(fileAppData);
}
},
[data, deleteAppData],
);

const contextValue: AppDataContextType = useMemo(
() => ({
postAppData: (payload: PostAppDataType) => {
Expand All @@ -159,22 +174,24 @@ export const AppDataProvider: FC<PropsWithChildren> = ({ children }) => {
appDataArray: data || List<AppData>(),
uploadFile,
getFileContent,
deleteFile,
status: {
isFetching,
isLoading,
isPreviousData,
},
}),
[
data,
patchAppData,
deleteAppData,
data,
uploadFile,
getFileContent,
deleteFile,
isFetching,
isLoading,
isPreviousData,
patchAppData,
postAppData,
uploadFile,
getFileContent,
],
);

Expand Down
67 changes: 67 additions & 0 deletions src/data/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { List } from 'immutable';

import { useCallback, useEffect, useState } from 'react';

import {
ExcalidrawImageElement,
FileId,
} from '@excalidraw/excalidraw/types/element/types';
import { BinaryFileData, DataURL } from '@excalidraw/excalidraw/types/types';

import { useAppDataContext } from '../components/context/AppDataContext';
import { FileAppData } from '../config/appDataTypes';

export const getListOfFileIds = (
elements: ExcalidrawImageElement[],
): List<string | undefined> =>
List(elements).map((element) => element?.fileId as string);

export const useFiles = (filesAppData: List<FileAppData>): BinaryFileData[] => {
const [files, setFiles] = useState<BinaryFileData[]>([]);
const { getFileContent } = useAppDataContext();
const getFile = useCallback(
async (fileAppData: FileAppData): Promise<BinaryFileData> => {
const { id, data: fileAppDataData } = fileAppData;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const reader = (file: any): Promise<DataURL> =>
new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = () => resolve(fr.result as DataURL);
fr.onerror = (err) => reject(err);
fr.readAsDataURL(file);
});
const { name: fileName, mimetype } = fileAppDataData.extra.s3File;
const fileData = await getFileContent(id || '').then((result) =>
reader(result),
);
const file: BinaryFileData = {
mimeType: mimetype as BinaryFileData['mimeType'],
id: fileName as FileId,
dataURL: fileData,
created: Date.now(),
};

return file;
},
[getFileContent],
);

const getFiles = useCallback(
(filesAppDataToAdd: List<FileAppData>): Promise<BinaryFileData[]> => {
const promisedFiles = filesAppDataToAdd.map(getFile);
return Promise.all(promisedFiles).then((result) => result);
},
[getFile],
);

useEffect(() => {
const filesToDownload = filesAppData.filterNot(({ data }) =>
files.map(({ id }) => id as string).includes(data.name),
);
if (!filesToDownload.isEmpty()) {
getFiles(filesToDownload).then((binaryFiles) => setFiles(binaryFiles));
}
}, [files, filesAppData, getFiles]);

return files;
};

0 comments on commit 33c310a

Please sign in to comment.