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:
commit
a58e2b81f7
5
.changeset/four-brooms-decide.md
Normal file
5
.changeset/four-brooms-decide.md
Normal 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
|
||||
@ -1,17 +1,20 @@
|
||||
import { CSSProperties, ReactNode, useEffect, useState } from 'react';
|
||||
import LoadingSpinner from './LoadingSpinner';
|
||||
|
||||
export interface BucketViewerFilterSearchedDataEvent {
|
||||
export interface BucketViewerFilterSearchedDataEvent {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
|
||||
export interface BucketViewerProps {
|
||||
onLoadTotalNumberOfItems?: (total: number) => void;
|
||||
domain: string;
|
||||
downloadConfig?: {
|
||||
downloadingMessageComponent?: ReactNode;
|
||||
hoverOfTheFileComponent?: ReactNode;
|
||||
};
|
||||
suffix?: string;
|
||||
className?: string;
|
||||
downloadComponent?: ReactNode;
|
||||
paginationConfig?: BucketViewerPaginationConfig;
|
||||
filterState?: BucketViewerFilterSearchedDataEvent;
|
||||
dataMapperFn: (rawData: Response) => Promise<BucketViewerData[]>;
|
||||
@ -34,20 +37,24 @@ export interface BucketViewerData {
|
||||
|
||||
export function BucketViewer({
|
||||
domain,
|
||||
downloadComponent,
|
||||
suffix,
|
||||
dataMapperFn,
|
||||
className,
|
||||
filterState,
|
||||
paginationConfig,
|
||||
onLoadTotalNumberOfItems
|
||||
downloadConfig,
|
||||
onLoadTotalNumberOfItems,
|
||||
}: BucketViewerProps) {
|
||||
|
||||
suffix = suffix ?? '/';
|
||||
downloadComponent = downloadComponent ?? <></>;
|
||||
|
||||
const { downloadingMessageComponent, hoverOfTheFileComponent } =
|
||||
downloadConfig ?? {};
|
||||
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 [lastPage, setLastPage] = useState<number>(0);
|
||||
const [bucketFiles, setBucketFiles] = useState<BucketViewerData[]>([]);
|
||||
@ -65,56 +72,59 @@ export function BucketViewer({
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [domain, suffix]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if(paginationConfig) {
|
||||
const startIndex = paginationConfig
|
||||
? currentPage * paginationConfig.itemsPerPage
|
||||
: 0;
|
||||
useEffect(() => {
|
||||
if (paginationConfig) {
|
||||
const startIndex = paginationConfig
|
||||
? currentPage * paginationConfig.itemsPerPage
|
||||
: 0;
|
||||
|
||||
const endIndex = paginationConfig
|
||||
? startIndex + paginationConfig.itemsPerPage
|
||||
: 0;
|
||||
const endIndex = paginationConfig
|
||||
? startIndex + paginationConfig.itemsPerPage
|
||||
: 0;
|
||||
|
||||
setLastPage(Math.ceil(filteredData.length / paginationConfig.itemsPerPage) - 1);
|
||||
setPaginatedData(filteredData.slice(startIndex, endIndex));
|
||||
}
|
||||
},
|
||||
[currentPage, filteredData]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if(onLoadTotalNumberOfItems) onLoadTotalNumberOfItems(filteredData.length);
|
||||
},
|
||||
[filteredData]
|
||||
)
|
||||
setLastPage(
|
||||
Math.ceil(filteredData.length / paginationConfig.itemsPerPage) - 1
|
||||
);
|
||||
setPaginatedData(filteredData.slice(startIndex, endIndex));
|
||||
}
|
||||
}, [currentPage, filteredData]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!filterState) return;
|
||||
if (onLoadTotalNumberOfItems) onLoadTotalNumberOfItems(filteredData.length);
|
||||
}, [filteredData]);
|
||||
|
||||
if (filterState.startDate && filterState.endDate) {
|
||||
setFilteredData(bucketFiles.filter(({ dateProps }) =>
|
||||
dateProps
|
||||
?
|
||||
dateProps.date.getTime() >= filterState.startDate.getTime()
|
||||
&& dateProps.date.getTime() <= filterState.endDate.getTime()
|
||||
useEffect(() => {
|
||||
if (!filterState) return;
|
||||
|
||||
if (filterState.startDate && filterState.endDate) {
|
||||
setFilteredData(
|
||||
bucketFiles.filter(({ dateProps }) =>
|
||||
dateProps
|
||||
? dateProps.date.getTime() >= filterState.startDate.getTime() &&
|
||||
dateProps.date.getTime() <= filterState.endDate.getTime()
|
||||
: true
|
||||
));
|
||||
} else if(filterState.startDate) {
|
||||
setFilteredData(bucketFiles.filter(({ dateProps }) =>
|
||||
dateProps ? dateProps.date.getTime() >= filterState.startDate.getTime() : true
|
||||
));
|
||||
} else if(filterState.endDate) {
|
||||
setFilteredData(bucketFiles.filter(({ dateProps }) =>
|
||||
dateProps ? dateProps.date.getTime() <= filterState.endDate.getTime() : true
|
||||
));
|
||||
} else {
|
||||
setFilteredData(bucketFiles);
|
||||
}
|
||||
},
|
||||
[filterState]
|
||||
)
|
||||
)
|
||||
);
|
||||
} else if (filterState.startDate) {
|
||||
setFilteredData(
|
||||
bucketFiles.filter(({ dateProps }) =>
|
||||
dateProps
|
||||
? dateProps.date.getTime() >= filterState.startDate.getTime()
|
||||
: true
|
||||
)
|
||||
);
|
||||
} else if (filterState.endDate) {
|
||||
setFilteredData(
|
||||
bucketFiles.filter(({ dateProps }) =>
|
||||
dateProps
|
||||
? dateProps.date.getTime() <= filterState.endDate.getTime()
|
||||
: true
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setFilteredData(bucketFiles);
|
||||
}
|
||||
}, [filterState]);
|
||||
|
||||
return isLoading ? (
|
||||
<div className="w-full flex items-center justify-center h-[300px]">
|
||||
@ -122,67 +132,86 @@ export function BucketViewer({
|
||||
</div>
|
||||
) : bucketFiles ? (
|
||||
<>
|
||||
{...(paginationConfig && bucketFiles
|
||||
? paginatedData
|
||||
: filteredData
|
||||
)?.map((data, i) => (
|
||||
<ul
|
||||
onClick={() => {
|
||||
const anchorId = `download_anchor_${i}`;
|
||||
const a: HTMLAnchorElement =
|
||||
(document.getElementById(anchorId) as HTMLAnchorElement | null) ??
|
||||
document.createElement('a');
|
||||
a.id = anchorId;
|
||||
if (a.download) a.click();
|
||||
else {
|
||||
setIsLoading(true);
|
||||
fetch(data.downloadFileUri)
|
||||
.then((res) => res.blob())
|
||||
.then((res) => {
|
||||
a.href = URL.createObjectURL(res);
|
||||
a.download = res.name ?? data.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}
|
||||
}}
|
||||
key={i}
|
||||
onMouseEnter={() => setShowDownloadComponentOnLine(i)}
|
||||
onMouseLeave={() => setShowDownloadComponentOnLine(undefined)}
|
||||
className={`${
|
||||
className ??
|
||||
'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
{
|
||||
downloadComponent && showDownloadComponentOnLine === i
|
||||
? downloadComponent
|
||||
: <></>
|
||||
}
|
||||
<div>
|
||||
<li>{data.fileName}</li>
|
||||
{data.dateProps && data.dateProps.dateFormatter ? (
|
||||
<li>{data.dateProps.dateFormatter(data.dateProps.date)}</li>
|
||||
{...(paginationConfig && bucketFiles ? paginatedData : filteredData)?.map(
|
||||
(data, i) => (
|
||||
<ul
|
||||
onClick={() => {
|
||||
const anchorId = `download_anchor_${data.fileName} `;
|
||||
const a: HTMLAnchorElement =
|
||||
(document.getElementById(
|
||||
anchorId
|
||||
) as HTMLAnchorElement | null) ?? document.createElement('a');
|
||||
a.id = anchorId;
|
||||
if (a.download) a.click();
|
||||
else {
|
||||
setShowDownloadLoadingOnFile((lastState) => {
|
||||
lastState.set(data.fileName, true);
|
||||
return new Map(lastState);
|
||||
});
|
||||
fetch(data.downloadFileUri)
|
||||
.then((res) => res.blob())
|
||||
.then((res) => {
|
||||
setShowDownloadLoadingOnFile((lastState) => {
|
||||
lastState.set(data.fileName, false);
|
||||
return new Map(lastState);
|
||||
});
|
||||
a.href = URL.createObjectURL(res);
|
||||
a.download = res.name ?? data.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
});
|
||||
}
|
||||
}}
|
||||
key={i}
|
||||
onMouseEnter={() => setShowDownloadComponentOnLine(i)}
|
||||
onMouseLeave={() => setShowDownloadComponentOnLine(undefined)}
|
||||
className={`${
|
||||
className ??
|
||||
'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
{hoverOfTheFileComponent && showDownloadComponentOnLine === i ? (
|
||||
hoverOfTheFileComponent
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</ul>
|
||||
))}
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<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 ? (
|
||||
<ul className={
|
||||
paginationConfig.containerClassName
|
||||
? paginationConfig.containerClassName
|
||||
: "flex justify-end gap-x-[0.5rem] w-full"
|
||||
}
|
||||
style={paginationConfig.containerStyles ?? {}}
|
||||
>
|
||||
<ul
|
||||
className={
|
||||
paginationConfig.containerClassName
|
||||
? paginationConfig.containerClassName
|
||||
: 'flex justify-end gap-x-[0.5rem] w-full'
|
||||
}
|
||||
style={paginationConfig.containerStyles ?? {}}
|
||||
>
|
||||
<li>
|
||||
<button
|
||||
<button
|
||||
className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
|
||||
disabled={currentPage === 0}
|
||||
onClick={() => setCurrentPage(0)}>First</button>
|
||||
onClick={() => setCurrentPage(0)}
|
||||
>
|
||||
First
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
@ -193,9 +222,7 @@ export function BucketViewer({
|
||||
Previous
|
||||
</button>
|
||||
</li>
|
||||
<label>
|
||||
{currentPage + 1}
|
||||
</label>
|
||||
<label>{currentPage + 1}</label>
|
||||
|
||||
<li>
|
||||
<button
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
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';
|
||||
|
||||
// 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'],
|
||||
argTypes: {
|
||||
domain: {
|
||||
description:
|
||||
'Bucket domain URI',
|
||||
description: 'Bucket domain URI',
|
||||
},
|
||||
suffix: {
|
||||
description:
|
||||
'Suffix of bucket domain',
|
||||
description: 'Suffix of bucket domain',
|
||||
},
|
||||
downloadComponent: {
|
||||
description:
|
||||
'Component to be displayed on hover of each bucket data',
|
||||
downloadConfig: {
|
||||
description: `Bucket file download configuration`,
|
||||
},
|
||||
filterState: {
|
||||
description: `State with values used to filter the bucket files`
|
||||
description: `State with values used to filter the bucket files`,
|
||||
},
|
||||
paginationConfig: {
|
||||
description: `Configuration to show and stylise the pagination on the component`,
|
||||
@ -42,17 +42,15 @@ export const Normal: Story = {
|
||||
suffix: '/',
|
||||
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()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return result.objects.map((e) => ({
|
||||
downloadFileUri: e.downloadLink,
|
||||
fileName: e.key.replace(/^(\w+\/)/g, ''),
|
||||
dateProps: {
|
||||
date: new Date(e.uploaded),
|
||||
dateFormatter: (date) => date.toLocaleDateString(),
|
||||
},
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -62,21 +60,19 @@ export const WithPagination: Story = {
|
||||
domain: 'https://ssen-smart-meter.datopian.workers.dev',
|
||||
suffix: '/',
|
||||
paginationConfig: {
|
||||
itemsPerPage: 3
|
||||
itemsPerPage: 3,
|
||||
},
|
||||
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()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return result.objects.map((e) => ({
|
||||
downloadFileUri: e.downloadLink,
|
||||
fileName: e.key.replace(/^(\w+\/)/g, ''),
|
||||
dateProps: {
|
||||
date: new Date(e.uploaded),
|
||||
dateFormatter: (date) => date.toLocaleDateString(),
|
||||
},
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -85,19 +81,37 @@ export const WithComponentOnHoverOfEachBucketFile: Story = {
|
||||
args: {
|
||||
domain: 'https://ssen-smart-meter.datopian.workers.dev',
|
||||
suffix: '/',
|
||||
downloadComponent: LoadingSpinner(),
|
||||
downloadConfig: { hoverOfTheFileComponent: `HOVER 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()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return result.objects.map((e) => ({
|
||||
downloadFileUri: e.downloadLink,
|
||||
fileName: e.key.replace(/^(\w+\/)/g, ''),
|
||||
dateProps: {
|
||||
date: new Date(e.uploaded),
|
||||
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(),
|
||||
},
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user