diff --git a/desktop/renderer-app/src/pages/CloudStoragePage/store.tsx b/desktop/renderer-app/src/pages/CloudStoragePage/store.tsx index dbbbb6aaec4..0fc9adf5efa 100644 --- a/desktop/renderer-app/src/pages/CloudStoragePage/store.tsx +++ b/desktop/renderer-app/src/pages/CloudStoragePage/store.tsx @@ -51,6 +51,13 @@ export class CloudStorageStore extends CloudStorageStoreBase { public insertCourseware: (file: CloudStorageFile) => void; public onCoursewareInserted?: () => void; + public cloudStorageSinglePageFiles = 50; + + /** In order to avoid multiple calls the fetchMoreCloudStorageData + * when request fetchMoreCloudStorageData after files length is 0 */ + public hasMoreFile = false; + public isFetchingFiles = false; + // a set of taskUUIDs representing querying tasks private convertStatusManager = new ConvertStatusManager(); @@ -73,12 +80,15 @@ export class CloudStorageStore extends CloudStorageStoreBase { makeObservable(this, { filesMap: observable, + isFetchingFiles: observable, pendingUploadTasks: computed, uploadingUploadTasks: computed, successUploadTasks: computed, failedUploadTasks: computed, files: computed, + currentFilesLength: computed, + cloudStorageDataPagination: computed, fileMenus: action, onItemMenuClick: action, @@ -113,6 +123,14 @@ export class CloudStorageStore extends CloudStorageStoreBase { return observable.array([...this.filesMap.values()]); } + public get currentFilesLength(): number { + return this.filesMap.size; + } + + public get cloudStorageDataPagination(): number { + return Math.ceil(this.currentFilesLength / this.cloudStorageSinglePageFiles); + } + /** Render file menus item base on fileUUID */ public fileMenus = ( file: CloudStorageFileUI, @@ -278,6 +296,62 @@ export class CloudStorageStore extends CloudStorageStoreBase { this.uploadTaskManager.addTasks(Array.from(files)); } + public getFilesLength = (): number => { + return this.filesMap.size; + }; + + public fetchMoreCloudStorageData = async (page: number): Promise => { + if (this.isFetchingFiles) { + return; + } + + const cloudStorageTotalPagesFilesCount = + this.cloudStorageDataPagination * this.cloudStorageSinglePageFiles; + + if (this.currentFilesLength >= cloudStorageTotalPagesFilesCount && this.hasMoreFile) { + this.isFetchingFiles = true; + + try { + const { files: cloudFiles } = await listFiles({ + page, + order: "DESC", + }); + + this.isFetchingFiles = false; + + this.hasMoreFile = cloudFiles.length === 0; + + for (const cloudFile of cloudFiles) { + const file = this.filesMap.get(cloudFile.fileUUID); + + runInAction(() => { + if (file) { + file.fileName = cloudFile.fileName; + file.createAt = cloudFile.createAt; + file.fileSize = cloudFile.fileSize; + file.convert = CloudStorageStore.mapConvertStep(cloudFile.convertStep); + file.taskToken = cloudFile.taskToken; + file.taskUUID = cloudFile.taskUUID; + file.external = cloudFile.external; + } else { + this.filesMap.set( + cloudFile.fileUUID, + observable.object({ + ...cloudFile, + convert: CloudStorageStore.mapConvertStep( + cloudFile.convertStep, + ), + }), + ); + } + }); + } + } catch { + this.isFetchingFiles = false; + } + } + }; + public initialize({ onCoursewareInserted, }: { onCoursewareInserted?: () => void } = {}): () => void { @@ -297,6 +371,7 @@ export class CloudStorageStore extends CloudStorageStoreBase { (currLen, prevLen) => { if (currLen < prevLen) { this.refreshFilesNowDebounced(); + this.hasMoreFile = true; } }, ); diff --git a/packages/flat-components/src/components/CloudStorage/CloudStorageFileList/index.tsx b/packages/flat-components/src/components/CloudStorage/CloudStorageFileList/index.tsx index 5570d31c8bf..079217365f9 100644 --- a/packages/flat-components/src/components/CloudStorage/CloudStorageFileList/index.tsx +++ b/packages/flat-components/src/components/CloudStorage/CloudStorageFileList/index.tsx @@ -13,6 +13,7 @@ import { CloudStorageFileListFileNameProps, } from "./CloudStorageFileListFileName"; import { useTranslation } from "react-i18next"; +import { SVGListLoading } from "../../FlatIcons"; export interface CloudStorageFileListProps extends Pick< @@ -28,6 +29,7 @@ export interface CloudStorageFileListProps files: CloudStorageFile[]; /** User selected file UUIDs */ selectedFileUUIDs: string[]; + isLoadingData: Boolean; /** Fires when user select or deselect files */ onSelectionChange: (fileUUID: string[]) => void; } @@ -44,6 +46,7 @@ export const CloudStorageFileList: React.FC = ({ titleClickable = false, onItemTitleClick, renamingFileUUID, + isLoadingData, onRename, }) => { const { t } = useTranslation(); @@ -138,6 +141,11 @@ export const CloudStorageFileList: React.FC = ({ }} size="small" /> + {isLoadingData && ( +
+ +
+ )} {files.length <= 0 && (
diff --git a/packages/flat-components/src/components/CloudStorage/CloudStorageFileList/style.less b/packages/flat-components/src/components/CloudStorage/CloudStorageFileList/style.less index 9e9dd46a672..b38ce24b947 100644 --- a/packages/flat-components/src/components/CloudStorage/CloudStorageFileList/style.less +++ b/packages/flat-components/src/components/CloudStorage/CloudStorageFileList/style.less @@ -56,6 +56,10 @@ } } +.cloud-storage-pull-up-loading { + margin: 0 auto; +} + .cloud-storage-file-list-filename-container { position: relative; display: flex; diff --git a/packages/flat-components/src/components/FlatIcons/FlatIcon.stories.tsx b/packages/flat-components/src/components/FlatIcons/FlatIcon.stories.tsx index 6896579521e..a7089616133 100644 --- a/packages/flat-components/src/components/FlatIcons/FlatIcon.stories.tsx +++ b/packages/flat-components/src/components/FlatIcons/FlatIcon.stories.tsx @@ -130,6 +130,7 @@ export const SuggestedIcons: Story = ({ active }) => ( "CircleInfoOutlined", "CircleInfoFilled", "Load", + "ListLoading", ]} title="Suggested Icons" /> diff --git a/packages/flat-components/src/components/FlatIcons/icons/SVGListLoading.tsx b/packages/flat-components/src/components/FlatIcons/icons/SVGListLoading.tsx new file mode 100644 index 00000000000..6ee04539dc9 --- /dev/null +++ b/packages/flat-components/src/components/FlatIcons/icons/SVGListLoading.tsx @@ -0,0 +1,28 @@ +import "../style.less"; +import React from "react"; +import { FlatIconProps } from "../types"; + +export const SVGListLoading: React.FC = ({ + active, + className = "", + ...restProps +}) => { + return ( + + + + + + + ); +}; + +export default SVGListLoading; diff --git a/packages/flat-components/src/components/FlatIcons/index.ts b/packages/flat-components/src/components/FlatIcons/index.ts index c8f9180ce77..74017943dd0 100644 --- a/packages/flat-components/src/components/FlatIcons/index.ts +++ b/packages/flat-components/src/components/FlatIcons/index.ts @@ -75,6 +75,7 @@ export * from "./icons/SVGHomeOutlined"; export * from "./icons/SVGInfo"; export * from "./icons/SVGLast"; export * from "./icons/SVGLeft"; +export * from "./icons/SVGListLoading"; export * from "./icons/SVGLoad"; export * from "./icons/SVGLoop"; export * from "./icons/SVGMenuFold"; diff --git a/packages/flat-components/src/containers/CloudStorageContainer/CloudStorageFileListContainer.tsx b/packages/flat-components/src/containers/CloudStorageContainer/CloudStorageFileListContainer.tsx index eac4d032690..120d0da09d3 100644 --- a/packages/flat-components/src/containers/CloudStorageContainer/CloudStorageFileListContainer.tsx +++ b/packages/flat-components/src/containers/CloudStorageContainer/CloudStorageFileListContainer.tsx @@ -6,15 +6,17 @@ import { CloudStorageFileList } from "../../components/CloudStorage"; export interface CloudStorageFileListContainerProps { store: CloudStorageStore; + isLoadingData: Boolean; } export const CloudStorageFileListContainer = observer( - function CloudStorageFileListContainer({ store }) { + function CloudStorageFileListContainer({ store, isLoadingData }) { return ( ): void => { export const CloudStorageContainer = observer( function CloudStorageContainer({ store }) { const { t } = useTranslation(); - + const sp = useSafePromise(); + const cloudStorageContainerRef = useRef(null); const [isH5PanelVisible, setH5PanelVisible] = useState(false); + const [isAtTheBottom, setIsAtTheBottom] = useState(false); + + useEffect(() => { + if (isAtTheBottom) { + void sp(store.fetchMoreCloudStorageData(store.cloudStorageDataPagination + 1)); + } + }, [isAtTheBottom, sp, store]); const handleMenuClick = useCallback(({ key }: { key: string }) => { if (key === "h5") { @@ -99,6 +108,18 @@ export const CloudStorageContainer = observer(
); + const onCloudStorageListScroll = (): void => { + if (cloudStorageContainerRef.current) { + const scrollViewOffsetY = cloudStorageContainerRef.current.scrollTop; + const scrollViewFrameHeight = cloudStorageContainerRef.current.clientHeight; + const scrollViewContentHeight = cloudStorageContainerRef.current.scrollHeight; + + setIsAtTheBottom( + scrollViewOffsetY + scrollViewFrameHeight >= scrollViewContentHeight, + ); + } + }; + return (
{!store.compact && ( @@ -118,9 +139,16 @@ export const CloudStorageContainer = observer( {containerBtns}
)} -
+
{store.totalUsageHR ? ( - + ) : ( )} diff --git a/packages/flat-components/src/containers/CloudStorageContainer/store.ts b/packages/flat-components/src/containers/CloudStorageContainer/store.ts index d8a41340f59..bae15db23fb 100644 --- a/packages/flat-components/src/containers/CloudStorageContainer/store.ts +++ b/packages/flat-components/src/containers/CloudStorageContainer/store.ts @@ -122,6 +122,9 @@ export abstract class CloudStorageStore { /** User cloud storage files */ public abstract files: CloudStorageFile[]; + /** get fetch data pagination value of cloudStorage. */ + public abstract cloudStorageDataPagination: number; + /** Render file menus item base on fileUUID */ public abstract fileMenus: ( file: CloudStorageFile, @@ -157,4 +160,16 @@ export abstract class CloudStorageStore { /** When file(s) are dropped in the container. */ public abstract onDropFile(files: FileList): void; + + /** When file(s) are dropped in the container. */ + public abstract onDropFile(files: FileList): void; + + /** In order to avoid multiple calls the fetchMoreCloudStorageData when fetching data */ + public abstract isFetchingFiles: boolean; + + /** Cloud storage single page data returned by the server */ + public abstract cloudStorageSinglePageFiles: number; + + /** When cloudStorage files is 50 or more, pull up to bottom that loading will fetch more pagination Data of the cloudStorage. */ + public abstract fetchMoreCloudStorageData: (page: number) => Promise; } diff --git a/web/flat-web/src/pages/CloudStoragePage/store.tsx b/web/flat-web/src/pages/CloudStoragePage/store.tsx index d5b2158f55d..389e87edbfd 100644 --- a/web/flat-web/src/pages/CloudStoragePage/store.tsx +++ b/web/flat-web/src/pages/CloudStoragePage/store.tsx @@ -51,6 +51,13 @@ export class CloudStorageStore extends CloudStorageStoreBase { public insertCourseware: (file: CloudStorageFile) => void; public onCoursewareInserted?: () => void; + public cloudStorageSinglePageFiles = 50; + + /** In order to avoid multiple calls the fetchMoreCloudStorageData + * when request fetchMoreCloudStorageData after files length is 0 */ + public hasMoreFile = true; + public isFetchingFiles = false; + // a set of taskUUIDs representing querying tasks private convertStatusManager = new ConvertStatusManager(); @@ -73,12 +80,15 @@ export class CloudStorageStore extends CloudStorageStoreBase { makeObservable(this, { filesMap: observable, + isFetchingFiles: observable, pendingUploadTasks: computed, uploadingUploadTasks: computed, successUploadTasks: computed, failedUploadTasks: computed, files: computed, + currentFilesLength: computed, + cloudStorageDataPagination: computed, fileMenus: action, onItemMenuClick: action, @@ -113,6 +123,14 @@ export class CloudStorageStore extends CloudStorageStoreBase { return observable.array([...this.filesMap.values()]); } + public get currentFilesLength(): number { + return this.filesMap.size; + } + + public get cloudStorageDataPagination(): number { + return Math.ceil(this.currentFilesLength / this.cloudStorageSinglePageFiles); + } + /** Render file menus item base on fileUUID */ public fileMenus = ( file: CloudStorageFileUI, @@ -278,6 +296,58 @@ export class CloudStorageStore extends CloudStorageStoreBase { this.uploadTaskManager.addTasks(Array.from(files)); } + public fetchMoreCloudStorageData = async (page: number): Promise => { + if (this.isFetchingFiles) { + return; + } + + const cloudStorageTotalPagesFilesCount = + this.cloudStorageDataPagination * this.cloudStorageSinglePageFiles; + + if (this.currentFilesLength >= cloudStorageTotalPagesFilesCount && this.hasMoreFile) { + this.isFetchingFiles = true; + + try { + const { files: cloudFiles } = await listFiles({ + page, + order: "DESC", + }); + + this.isFetchingFiles = false; + + this.hasMoreFile = cloudFiles.length === 0; + + for (const cloudFile of cloudFiles) { + const file = this.filesMap.get(cloudFile.fileUUID); + + runInAction(() => { + if (file) { + file.fileName = cloudFile.fileName; + file.createAt = cloudFile.createAt; + file.fileSize = cloudFile.fileSize; + file.convert = CloudStorageStore.mapConvertStep(cloudFile.convertStep); + file.taskToken = cloudFile.taskToken; + file.taskUUID = cloudFile.taskUUID; + file.external = cloudFile.external; + } else { + this.filesMap.set( + cloudFile.fileUUID, + observable.object({ + ...cloudFile, + convert: CloudStorageStore.mapConvertStep( + cloudFile.convertStep, + ), + }), + ); + } + }); + } + } catch { + this.isFetchingFiles = false; + } + } + }; + public initialize({ onCoursewareInserted, }: { onCoursewareInserted?: () => void } = {}): () => void { @@ -297,6 +367,7 @@ export class CloudStorageStore extends CloudStorageStoreBase { (currLen, prevLen) => { if (currLen < prevLen) { this.refreshFilesNowDebounced(); + this.hasMoreFile = true; } }, );