Merge pull request #1077 from datopian/feature/download-loading-message

Created property to present a component while loading the download of the file and fixed download bug on pagination
This commit is contained in:
Leonardo Yuri Farias 2024-01-24 17:20:09 -03:00 committed by GitHub
commit a58e2b81f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 199 additions and 153 deletions

View File

@ -0,0 +1,5 @@
---
'@portaljs/components': patch
---
Created property to present a component while is loading the download of the file and fixed download bug on pagination

View File

@ -1,17 +1,20 @@
import { CSSProperties, ReactNode, useEffect, useState } from 'react'; import { CSSProperties, ReactNode, useEffect, useState } from 'react';
import LoadingSpinner from './LoadingSpinner'; import LoadingSpinner from './LoadingSpinner';
export interface BucketViewerFilterSearchedDataEvent { export interface BucketViewerFilterSearchedDataEvent {
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
} }
export interface BucketViewerProps { export interface BucketViewerProps {
onLoadTotalNumberOfItems?: (total: number) => void; onLoadTotalNumberOfItems?: (total: number) => void;
domain: string; domain: string;
downloadConfig?: {
downloadingMessageComponent?: ReactNode;
hoverOfTheFileComponent?: ReactNode;
};
suffix?: string; suffix?: string;
className?: string; className?: string;
downloadComponent?: ReactNode;
paginationConfig?: BucketViewerPaginationConfig; paginationConfig?: BucketViewerPaginationConfig;
filterState?: BucketViewerFilterSearchedDataEvent; filterState?: BucketViewerFilterSearchedDataEvent;
dataMapperFn: (rawData: Response) => Promise<BucketViewerData[]>; dataMapperFn: (rawData: Response) => Promise<BucketViewerData[]>;
@ -34,20 +37,24 @@ export interface BucketViewerData {
export function BucketViewer({ export function BucketViewer({
domain, domain,
downloadComponent,
suffix, suffix,
dataMapperFn, dataMapperFn,
className, className,
filterState, filterState,
paginationConfig, paginationConfig,
onLoadTotalNumberOfItems downloadConfig,
onLoadTotalNumberOfItems,
}: BucketViewerProps) { }: BucketViewerProps) {
suffix = suffix ?? '/'; suffix = suffix ?? '/';
downloadComponent = downloadComponent ?? <></>;
const { downloadingMessageComponent, hoverOfTheFileComponent } =
downloadConfig ?? {};
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [showDownloadComponentOnLine, setShowDownloadComponentOnLine] = useState(-1); const [showDownloadComponentOnLine, setShowDownloadComponentOnLine] =
useState(-1);
const [showDownloadLoadingOnFile, setShowDownloadLoadingOnFile] = useState(
new Map<string, boolean>()
);
const [currentPage, setCurrentPage] = useState<number>(0); const [currentPage, setCurrentPage] = useState<number>(0);
const [lastPage, setLastPage] = useState<number>(0); const [lastPage, setLastPage] = useState<number>(0);
const [bucketFiles, setBucketFiles] = useState<BucketViewerData[]>([]); const [bucketFiles, setBucketFiles] = useState<BucketViewerData[]>([]);
@ -65,56 +72,59 @@ export function BucketViewer({
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
}, [domain, suffix]); }, [domain, suffix]);
useEffect( useEffect(() => {
() => { if (paginationConfig) {
if(paginationConfig) { const startIndex = paginationConfig
const startIndex = paginationConfig ? currentPage * paginationConfig.itemsPerPage
? currentPage * paginationConfig.itemsPerPage : 0;
: 0;
const endIndex = paginationConfig const endIndex = paginationConfig
? startIndex + paginationConfig.itemsPerPage ? startIndex + paginationConfig.itemsPerPage
: 0; : 0;
setLastPage(Math.ceil(filteredData.length / paginationConfig.itemsPerPage) - 1); setLastPage(
setPaginatedData(filteredData.slice(startIndex, endIndex)); Math.ceil(filteredData.length / paginationConfig.itemsPerPage) - 1
} );
}, setPaginatedData(filteredData.slice(startIndex, endIndex));
[currentPage, filteredData] }
); }, [currentPage, filteredData]);
useEffect(
() => {
if(onLoadTotalNumberOfItems) onLoadTotalNumberOfItems(filteredData.length);
},
[filteredData]
)
useEffect(() => { useEffect(() => {
if(!filterState) return; if (onLoadTotalNumberOfItems) onLoadTotalNumberOfItems(filteredData.length);
}, [filteredData]);
if (filterState.startDate && filterState.endDate) { useEffect(() => {
setFilteredData(bucketFiles.filter(({ dateProps }) => if (!filterState) return;
dateProps
? if (filterState.startDate && filterState.endDate) {
dateProps.date.getTime() >= filterState.startDate.getTime() setFilteredData(
&& dateProps.date.getTime() <= filterState.endDate.getTime() bucketFiles.filter(({ dateProps }) =>
dateProps
? dateProps.date.getTime() >= filterState.startDate.getTime() &&
dateProps.date.getTime() <= filterState.endDate.getTime()
: true : true
)); )
} else if(filterState.startDate) { );
setFilteredData(bucketFiles.filter(({ dateProps }) => } else if (filterState.startDate) {
dateProps ? dateProps.date.getTime() >= filterState.startDate.getTime() : true setFilteredData(
)); bucketFiles.filter(({ dateProps }) =>
} else if(filterState.endDate) { dateProps
setFilteredData(bucketFiles.filter(({ dateProps }) => ? dateProps.date.getTime() >= filterState.startDate.getTime()
dateProps ? dateProps.date.getTime() <= filterState.endDate.getTime() : true : true
)); )
} else { );
setFilteredData(bucketFiles); } else if (filterState.endDate) {
} setFilteredData(
}, bucketFiles.filter(({ dateProps }) =>
[filterState] dateProps
) ? dateProps.date.getTime() <= filterState.endDate.getTime()
: true
)
);
} else {
setFilteredData(bucketFiles);
}
}, [filterState]);
return isLoading ? ( return isLoading ? (
<div className="w-full flex items-center justify-center h-[300px]"> <div className="w-full flex items-center justify-center h-[300px]">
@ -122,67 +132,86 @@ export function BucketViewer({
</div> </div>
) : bucketFiles ? ( ) : bucketFiles ? (
<> <>
{...(paginationConfig && bucketFiles {...(paginationConfig && bucketFiles ? paginatedData : filteredData)?.map(
? paginatedData (data, i) => (
: filteredData <ul
)?.map((data, i) => ( onClick={() => {
<ul const anchorId = `download_anchor_${data.fileName} `;
onClick={() => { const a: HTMLAnchorElement =
const anchorId = `download_anchor_${i}`; (document.getElementById(
const a: HTMLAnchorElement = anchorId
(document.getElementById(anchorId) as HTMLAnchorElement | null) ?? ) as HTMLAnchorElement | null) ?? document.createElement('a');
document.createElement('a'); a.id = anchorId;
a.id = anchorId; if (a.download) a.click();
if (a.download) a.click(); else {
else { setShowDownloadLoadingOnFile((lastState) => {
setIsLoading(true); lastState.set(data.fileName, true);
fetch(data.downloadFileUri) return new Map(lastState);
.then((res) => res.blob()) });
.then((res) => { fetch(data.downloadFileUri)
a.href = URL.createObjectURL(res); .then((res) => res.blob())
a.download = res.name ?? data.fileName; .then((res) => {
document.body.appendChild(a); setShowDownloadLoadingOnFile((lastState) => {
a.click(); lastState.set(data.fileName, false);
}) return new Map(lastState);
.finally(() => setIsLoading(false)); });
} a.href = URL.createObjectURL(res);
}} a.download = res.name ?? data.fileName;
key={i} document.body.appendChild(a);
onMouseEnter={() => setShowDownloadComponentOnLine(i)} a.click();
onMouseLeave={() => setShowDownloadComponentOnLine(undefined)} });
className={`${ }
className ?? }}
'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer' key={i}
}`} onMouseEnter={() => setShowDownloadComponentOnLine(i)}
> onMouseLeave={() => setShowDownloadComponentOnLine(undefined)}
{ className={`${
downloadComponent && showDownloadComponentOnLine === i className ??
? downloadComponent 'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer'
: <></> }`}
} >
<div> {hoverOfTheFileComponent && showDownloadComponentOnLine === i ? (
<li>{data.fileName}</li> hoverOfTheFileComponent
{data.dateProps && data.dateProps.dateFormatter ? (
<li>{data.dateProps.dateFormatter(data.dateProps.date)}</li>
) : ( ) : (
<></> <></>
)} )}
</div> <div className="flex justify-between w-full items-center">
</ul> <div>
))} <li>{data.fileName}</li>
{data.dateProps && data.dateProps.dateFormatter ? (
<li>{data.dateProps.dateFormatter(data.dateProps.date)}</li>
) : (
<></>
)}
</div>
{showDownloadLoadingOnFile.get(data.fileName) ? (
downloadingMessageComponent ?? (
<label>Downloading file...</label>
)
) : (
<></>
)}
</div>
</ul>
)
)}
{paginationConfig ? ( {paginationConfig ? (
<ul className={ <ul
paginationConfig.containerClassName className={
? paginationConfig.containerClassName paginationConfig.containerClassName
: "flex justify-end gap-x-[0.5rem] w-full" ? paginationConfig.containerClassName
} : 'flex justify-end gap-x-[0.5rem] w-full'
style={paginationConfig.containerStyles ?? {}} }
> style={paginationConfig.containerStyles ?? {}}
>
<li> <li>
<button <button
className="hover:cursor-pointer hover:disabled:cursor-not-allowed" className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
disabled={currentPage === 0} disabled={currentPage === 0}
onClick={() => setCurrentPage(0)}>First</button> onClick={() => setCurrentPage(0)}
>
First
</button>
</li> </li>
<li> <li>
<button <button
@ -193,9 +222,7 @@ export function BucketViewer({
Previous Previous
</button> </button>
</li> </li>
<label> <label>{currentPage + 1}</label>
{currentPage + 1}
</label>
<li> <li>
<button <button

View File

@ -1,6 +1,9 @@
import { type Meta, type StoryObj } from '@storybook/react'; import { type Meta, type StoryObj } from '@storybook/react';
import { BucketViewer, BucketViewerProps } from '../src/components/BucketViewer'; import {
BucketViewer,
BucketViewerProps,
} from '../src/components/BucketViewer';
import LoadingSpinner from '../src/components/LoadingSpinner'; import LoadingSpinner from '../src/components/LoadingSpinner';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
@ -10,19 +13,16 @@ const meta: Meta = {
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
domain: { domain: {
description: description: 'Bucket domain URI',
'Bucket domain URI',
}, },
suffix: { suffix: {
description: description: 'Suffix of bucket domain',
'Suffix of bucket domain',
}, },
downloadComponent: { downloadConfig: {
description: description: `Bucket file download configuration`,
'Component to be displayed on hover of each bucket data',
}, },
filterState: { filterState: {
description: `State with values used to filter the bucket files` description: `State with values used to filter the bucket files`,
}, },
paginationConfig: { paginationConfig: {
description: `Configuration to show and stylise the pagination on the component`, description: `Configuration to show and stylise the pagination on the component`,
@ -42,17 +42,15 @@ export const Normal: Story = {
suffix: '/', suffix: '/',
dataMapperFn: async (rawData: Response) => { dataMapperFn: async (rawData: Response) => {
const result = await rawData.json(); const result = await rawData.json();
return result.objects.map( return result.objects.map((e) => ({
e => ({ downloadFileUri: e.downloadLink,
downloadFileUri: e.downloadLink, fileName: e.key.replace(/^(\w+\/)/g, ''),
fileName: e.key.replace(/^(\w+\/)/g, '') , dateProps: {
dateProps: { date: new Date(e.uploaded),
date: new Date(e.uploaded), dateFormatter: (date) => date.toLocaleDateString(),
dateFormatter: (date) => date.toLocaleDateString() },
} }));
}) },
)
}
}, },
}; };
@ -62,21 +60,19 @@ export const WithPagination: Story = {
domain: 'https://ssen-smart-meter.datopian.workers.dev', domain: 'https://ssen-smart-meter.datopian.workers.dev',
suffix: '/', suffix: '/',
paginationConfig: { paginationConfig: {
itemsPerPage: 3 itemsPerPage: 3,
}, },
dataMapperFn: async (rawData: Response) => { dataMapperFn: async (rawData: Response) => {
const result = await rawData.json(); const result = await rawData.json();
return result.objects.map( return result.objects.map((e) => ({
e => ({ downloadFileUri: e.downloadLink,
downloadFileUri: e.downloadLink, fileName: e.key.replace(/^(\w+\/)/g, ''),
fileName: e.key.replace(/^(\w+\/)/g, '') , dateProps: {
dateProps: { date: new Date(e.uploaded),
date: new Date(e.uploaded), dateFormatter: (date) => date.toLocaleDateString(),
dateFormatter: (date) => date.toLocaleDateString() },
} }));
}) },
)
}
}, },
}; };
@ -85,19 +81,37 @@ export const WithComponentOnHoverOfEachBucketFile: Story = {
args: { args: {
domain: 'https://ssen-smart-meter.datopian.workers.dev', domain: 'https://ssen-smart-meter.datopian.workers.dev',
suffix: '/', suffix: '/',
downloadComponent: LoadingSpinner(), downloadConfig: { hoverOfTheFileComponent: `HOVER COMPONENT` },
dataMapperFn: async (rawData: Response) => { dataMapperFn: async (rawData: Response) => {
const result = await rawData.json(); const result = await rawData.json();
return result.objects.map( return result.objects.map((e) => ({
e => ({ downloadFileUri: e.downloadLink,
downloadFileUri: e.downloadLink, fileName: e.key.replace(/^(\w+\/)/g, ''),
fileName: e.key.replace(/^(\w+\/)/g, '') , dateProps: {
dateProps: { date: new Date(e.uploaded),
date: new Date(e.uploaded), dateFormatter: (date) => date.toLocaleDateString(),
dateFormatter: (date) => date.toLocaleDateString() },
} }));
}) },
) },
} };
export const WithLoadingComponentWhileDownloadTheBucketFile: Story = {
name: 'With loading component while download the bucket file',
args: {
domain: 'https://ssen-smart-meter.datopian.workers.dev',
suffix: '/',
downloadConfig: { downloadingMessageComponent: 'COMPONENT....' },
dataMapperFn: async (rawData: Response) => {
const result = await rawData.json();
return result.objects.map((e) => ({
downloadFileUri: e.downloadLink,
fileName: e.key.replace(/^(\w+\/)/g, ''),
dateProps: {
date: new Date(e.uploaded),
dateFormatter: (date) => date.toLocaleDateString(),
},
}));
},
}, },
}; };