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 { 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
|
||||||
|
|||||||
@ -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(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user