Compare commits
32 Commits
@portaljs/
...
external-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
806bc89e8c | ||
|
|
ed9ac2c263 | ||
|
|
42c72e5afd | ||
|
|
9e1a324fa1 | ||
|
|
90178af8f2 | ||
|
|
00e61e104c | ||
|
|
f7f03fddca | ||
|
|
0891dfde2d | ||
|
|
c904e3731b | ||
|
|
86a2945ee6 | ||
|
|
09daa98b28 | ||
|
|
b511c9f71b | ||
|
|
464cda6db8 | ||
|
|
2bbf313489 | ||
|
|
c26b76368d | ||
|
|
af11f0cfd5 | ||
|
|
9ae2b31113 | ||
|
|
2bffd130c8 | ||
|
|
058d23678a | ||
|
|
540a08934c | ||
|
|
7d010cfee4 | ||
|
|
dd79da1c6b | ||
|
|
a58e2b81f7 | ||
|
|
6d7acd27ed | ||
|
|
7c30842c7d | ||
|
|
35ca1d6dfd | ||
|
|
a7e90b64af | ||
|
|
26dcffc279 | ||
|
|
d18e3dd486 | ||
|
|
8d7059acb4 | ||
|
|
09d5324d4e | ||
|
|
cf24042a91 |
@@ -1,3 +1,9 @@
|
||||
# PortalJS Demo replicating the FiveThirtyEight data portal
|
||||
|
||||
## 👉 https://fivethirtyeight.portaljs.org 👈
|
||||
|
||||
Here's a blog post we wrote about it: https://www.datopian.com/blog/fivethirtyeight-replica
|
||||
|
||||
This is a replica of the awesome data.fivethirtyeight.com using PortalJS.
|
||||
|
||||
You might be asking why we did that, there are three main reasons:
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
# @portaljs/components
|
||||
|
||||
## 0.5.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1083](https://github.com/datopian/portaljs/pull/1083) [`86a2945e`](https://github.com/datopian/portaljs/commit/86a2945ee68dfcea0299984ca9cc9070d68fe1c2) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Created integration with datastore api for table component
|
||||
|
||||
## 0.5.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1081](https://github.com/datopian/portaljs/pull/1081) [`2bbf3134`](https://github.com/datopian/portaljs/commit/2bbf3134896df3ecc66560bdf95bece143614c7b) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Fixed error to remove anchor from document
|
||||
|
||||
## 0.5.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1079](https://github.com/datopian/portaljs/pull/1079) [`058d2367`](https://github.com/datopian/portaljs/commit/058d23678a024890f8a6d909ded9fc8fc11cf145) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Changed the download behaviour of the bucket viewer component and removed loading component while downloading
|
||||
|
||||
## 0.5.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1077](https://github.com/datopian/portaljs/pull/1077) [`6d7acd27`](https://github.com/datopian/portaljs/commit/6d7acd27ed9299cbcc14eab906f2f0eb414656b8) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Created property to present a component while is loading the download of the file and fixed download bug on pagination
|
||||
|
||||
## 0.5.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1075](https://github.com/datopian/portaljs/pull/1075) [`26dcffc2`](https://github.com/datopian/portaljs/commit/26dcffc279057f80a579134e862085ba042c06c3) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Fixed problem presenting the download component in the first load of the bucket viewer
|
||||
|
||||
## 0.5.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1073](https://github.com/datopian/portaljs/pull/1073) [`cf24042a`](https://github.com/datopian/portaljs/commit/cf24042a910567e98eeb75ade42ce0149bdb62d1) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Fixed filter by startDate error
|
||||
|
||||
## 0.5.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@portaljs/components",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.10",
|
||||
"type": "module",
|
||||
"description": "https://portaljs.org",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
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?: {
|
||||
hoverOfTheFileComponent?: ReactNode;
|
||||
};
|
||||
suffix?: string;
|
||||
className?: string;
|
||||
downloadComponent?: ReactNode;
|
||||
paginationConfig?: BucketViewerPaginationConfig;
|
||||
filterState?: BucketViewerFilterSearchedDataEvent;
|
||||
dataMapperFn: (rawData: Response) => Promise<BucketViewerData[]>;
|
||||
@@ -34,20 +36,20 @@ export interface BucketViewerData {
|
||||
|
||||
export function BucketViewer({
|
||||
domain,
|
||||
downloadComponent,
|
||||
suffix,
|
||||
dataMapperFn,
|
||||
className,
|
||||
filterState,
|
||||
paginationConfig,
|
||||
onLoadTotalNumberOfItems
|
||||
downloadConfig,
|
||||
onLoadTotalNumberOfItems,
|
||||
}: BucketViewerProps) {
|
||||
|
||||
suffix = suffix ?? '/';
|
||||
downloadComponent = downloadComponent ?? <></>;
|
||||
|
||||
const { hoverOfTheFileComponent } = downloadConfig ?? {};
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [showDownloadComponentOnLine, setShowDownloadComponentOnLine] = useState(0);
|
||||
const [showDownloadComponentOnLine, setShowDownloadComponentOnLine] =
|
||||
useState(-1);
|
||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||
const [lastPage, setLastPage] = useState<number>(0);
|
||||
const [bucketFiles, setBucketFiles] = useState<BucketViewerData[]>([]);
|
||||
@@ -65,56 +67,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.startDate) {
|
||||
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 +127,61 @@ 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 a: HTMLAnchorElement = document.createElement('a');
|
||||
a.href = data.downloadFileUri;
|
||||
a.target = `_blank`;
|
||||
a.download = data.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}}
|
||||
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>
|
||||
</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 +192,7 @@ export function BucketViewer({
|
||||
Previous
|
||||
</button>
|
||||
</li>
|
||||
<label>
|
||||
{currentPage + 1}
|
||||
</label>
|
||||
<label>{currentPage + 1}</label>
|
||||
|
||||
<li>
|
||||
<button
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
PaginationState,
|
||||
Table as ReactTable,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
@@ -25,12 +27,19 @@ import DebouncedInput from './DebouncedInput';
|
||||
import loadData from '../lib/loadData';
|
||||
import LoadingSpinner from './LoadingSpinner';
|
||||
|
||||
export type TableData = { cols: {key: string, name: string}[]; data: any[]; total: number };
|
||||
|
||||
export type TableProps = {
|
||||
data?: Array<{ [key: string]: number | string }>;
|
||||
cols?: Array<{ [key: string]: string }>;
|
||||
csv?: string;
|
||||
url?: string;
|
||||
fullWidth?: boolean;
|
||||
datastoreConfig?: {
|
||||
dataStoreURI: string;
|
||||
rowsPerPage?: number;
|
||||
dataMapperFn: (data) => Promise<TableData> | TableData;
|
||||
};
|
||||
};
|
||||
|
||||
export const Table = ({
|
||||
@@ -39,8 +48,28 @@ export const Table = ({
|
||||
csv = '',
|
||||
url = '',
|
||||
fullWidth = false,
|
||||
datastoreConfig,
|
||||
}: TableProps) => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [pageMap, setPageMap] = useState(new Map<number, boolean>());
|
||||
const {
|
||||
dataMapperFn,
|
||||
dataStoreURI,
|
||||
rowsPerPage = 10,
|
||||
} = datastoreConfig ?? {};
|
||||
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
const [isLoadingPage, setIsLoadingPage] = useState<boolean>(false);
|
||||
const [totalOfRows, setTotalOfRows] = useState<number>(0);
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: rowsPerPage,
|
||||
});
|
||||
|
||||
const [lastIndex, setLastIndex] = useState(pageSize);
|
||||
const [startIndex, setStartIndex] = useState(0);
|
||||
const [hasSorted, setHasSorted] = useState(false);
|
||||
|
||||
if (csv) {
|
||||
const out = parseCsv(csv);
|
||||
@@ -62,21 +91,56 @@ export const Table = ({
|
||||
);
|
||||
}, [data, cols]);
|
||||
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
let table: ReactTable<unknown>;
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns: tableCols,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
state: {
|
||||
globalFilter,
|
||||
},
|
||||
globalFilterFn: globalFilterFn,
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
});
|
||||
if (datastoreConfig) {
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
fetch(`${dataStoreURI}&limit=${rowsPerPage}&offset=0`)
|
||||
.then((res) => res.json())
|
||||
.then(async (res) => {
|
||||
const { data, cols, total } = await dataMapperFn(res);
|
||||
setData(data);
|
||||
setCols(cols);
|
||||
setTotalOfRows(Math.ceil(total / rowsPerPage));
|
||||
pageMap.set(0, true);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [dataStoreURI]);
|
||||
|
||||
table = useReactTable({
|
||||
data,
|
||||
pageCount: totalOfRows,
|
||||
columns: tableCols,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
state: {
|
||||
pagination: { pageIndex, pageSize },
|
||||
},
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
manualPagination: true,
|
||||
onPaginationChange: setPagination,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasSorted) return;
|
||||
queryDataByText(globalFilter);
|
||||
}, [table.getState().sorting]);
|
||||
} else {
|
||||
table = useReactTable({
|
||||
data,
|
||||
columns: tableCols,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
state: {
|
||||
globalFilter,
|
||||
},
|
||||
globalFilterFn: globalFilterFn,
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
@@ -91,6 +155,70 @@ export const Table = ({
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
const queryDataByText = (filter) => {
|
||||
setIsLoadingPage(true);
|
||||
const sortedParam = getSortParam();
|
||||
fetch(
|
||||
`${dataStoreURI}&limit=${rowsPerPage}&offset=0&q=${filter}${sortedParam}`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then(async (res) => {
|
||||
const { data, total = 0 } = await dataMapperFn(res);
|
||||
setTotalOfRows(Math.ceil(total / rowsPerPage));
|
||||
setData(data);
|
||||
const newMap = new Map();
|
||||
newMap.set(0, true);
|
||||
setPageMap(newMap);
|
||||
table.setPageIndex(0);
|
||||
setStartIndex(0);
|
||||
setLastIndex(pageSize);
|
||||
})
|
||||
.finally(() => setIsLoadingPage(false));
|
||||
};
|
||||
|
||||
const getSortParam = () => {
|
||||
const sort = table.getState().sorting;
|
||||
return sort.length == 0
|
||||
? ``
|
||||
: '&sort=' +
|
||||
sort
|
||||
.map(
|
||||
(x, i) =>
|
||||
`${x.id}${
|
||||
i === sort.length - 1 ? (x.desc ? ` desc` : ` asc`) : `,`
|
||||
}`
|
||||
)
|
||||
.reduce((x1, x2) => x1 + x2);
|
||||
};
|
||||
|
||||
const queryPaginatedData = (newPageIndex) => {
|
||||
let newStartIndex = newPageIndex * pageSize;
|
||||
setStartIndex(newStartIndex);
|
||||
setLastIndex(newStartIndex + pageSize);
|
||||
|
||||
if (!pageMap.get(newPageIndex)) pageMap.set(newPageIndex, true);
|
||||
else return;
|
||||
|
||||
const sortedParam = getSortParam();
|
||||
|
||||
setIsLoadingPage(true);
|
||||
fetch(
|
||||
`${dataStoreURI}&limit=${rowsPerPage}&offset=${
|
||||
newStartIndex + pageSize
|
||||
}&q=${globalFilter}${sortedParam}`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then(async (res) => {
|
||||
const { data: responseData } = await dataMapperFn(res);
|
||||
responseData.forEach((e) => {
|
||||
data[newStartIndex] = e;
|
||||
newStartIndex++;
|
||||
});
|
||||
setData([...data]);
|
||||
})
|
||||
.finally(() => setIsLoadingPage(false));
|
||||
};
|
||||
|
||||
return isLoading ? (
|
||||
<div className="w-full h-full min-h-[500px] flex items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
@@ -99,7 +227,10 @@ export const Table = ({
|
||||
<div className={`${fullWidth ? 'w-[90vw] ml-[calc(50%-45vw)]' : 'w-full'}`}>
|
||||
<DebouncedInput
|
||||
value={globalFilter ?? ''}
|
||||
onChange={(value: any) => setGlobalFilter(String(value))}
|
||||
onChange={(value: any) => {
|
||||
if (datastoreConfig) queryDataByText(String(value));
|
||||
setGlobalFilter(String(value));
|
||||
}}
|
||||
className="p-2 text-sm shadow border border-block"
|
||||
placeholder="Search all columns..."
|
||||
/>
|
||||
@@ -114,7 +245,10 @@ export const Table = ({
|
||||
className: h.column.getCanSort()
|
||||
? 'cursor-pointer select-none'
|
||||
: '',
|
||||
onClick: h.column.getToggleSortingHandler(),
|
||||
onClick: (v) => {
|
||||
setHasSorted(true);
|
||||
h.column.getToggleSortingHandler()(v);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{flexRender(h.column.columnDef.header, h.getContext())}
|
||||
@@ -135,15 +269,28 @@ export const Table = ({
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((r) => (
|
||||
<tr key={r.id} className="border-b border-b-slate-200">
|
||||
{r.getVisibleCells().map((c) => (
|
||||
<td key={c.id} className="py-2">
|
||||
{flexRender(c.column.columnDef.cell, c.getContext())}
|
||||
</td>
|
||||
))}
|
||||
{datastoreConfig && isLoadingPage ? (
|
||||
<tr>
|
||||
<td colSpan={cols.length} rowSpan={cols.length}>
|
||||
<div className="w-full h-full flex items-center justify-center pt-6">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
) : (
|
||||
(datastoreConfig
|
||||
? table.getRowModel().rows.slice(startIndex, lastIndex)
|
||||
: table.getRowModel().rows
|
||||
).map((r) => (
|
||||
<tr key={r.id} className="border-b border-b-slate-200">
|
||||
{r.getVisibleCells().map((c) => (
|
||||
<td key={c.id} className="py-2">
|
||||
{flexRender(c.column.columnDef.cell, c.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex gap-2 items-center justify-center mt-10">
|
||||
@@ -151,7 +298,10 @@ export const Table = ({
|
||||
className={`w-6 h-6 ${
|
||||
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
|
||||
}`}
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
onClick={() => {
|
||||
if (datastoreConfig) queryPaginatedData(0);
|
||||
table.setPageIndex(0);
|
||||
}}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<ChevronDoubleLeftIcon />
|
||||
@@ -160,7 +310,12 @@ export const Table = ({
|
||||
className={`w-6 h-6 ${
|
||||
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
|
||||
}`}
|
||||
onClick={() => table.previousPage()}
|
||||
onClick={() => {
|
||||
if (datastoreConfig) {
|
||||
queryPaginatedData(table.getState().pagination.pageIndex - 1);
|
||||
}
|
||||
table.previousPage();
|
||||
}}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
@@ -176,7 +331,11 @@ export const Table = ({
|
||||
className={`w-6 h-6 ${
|
||||
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
|
||||
}`}
|
||||
onClick={() => table.nextPage()}
|
||||
onClick={() => {
|
||||
if (datastoreConfig)
|
||||
queryPaginatedData(table.getState().pagination.pageIndex + 1);
|
||||
table.nextPage();
|
||||
}}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
@@ -185,7 +344,11 @@ export const Table = ({
|
||||
className={`w-6 h-6 ${
|
||||
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
|
||||
}`}
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
onClick={() => {
|
||||
const pageIndexToNavigate = table.getPageCount() - 1;
|
||||
if (datastoreConfig) queryPaginatedData(pageIndexToNavigate);
|
||||
table.setPageIndex(pageIndexToNavigate);
|
||||
}}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<ChevronDoubleRightIcon />
|
||||
|
||||
@@ -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,17 @@ 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(),
|
||||
},
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,17 +9,22 @@ const meta: Meta = {
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
data: {
|
||||
description: "Data to be displayed in the table, must also set \"cols\" to work."
|
||||
description:
|
||||
'Data to be displayed in the table, must also set "cols" to work.',
|
||||
},
|
||||
cols: {
|
||||
description: "Columns to be displayed in the table, must also set \"data\" to work."
|
||||
description:
|
||||
'Columns to be displayed in the table, must also set "data" to work.',
|
||||
},
|
||||
csv: {
|
||||
description: "CSV data as string.",
|
||||
description: 'CSV data as string.',
|
||||
},
|
||||
url: {
|
||||
description: "Fetch the data from a CSV file remotely."
|
||||
}
|
||||
description: 'Fetch the data from a CSV file remotely.',
|
||||
},
|
||||
datastoreConfig: {
|
||||
description: `Configuration to use CKAN's datastore API extension integrated with the component`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -29,7 +34,7 @@ type Story = StoryObj<TableProps>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
export const FromColumnsAndData: Story = {
|
||||
name: "Table from columns and data",
|
||||
name: 'Table from columns and data',
|
||||
args: {
|
||||
data: [
|
||||
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
|
||||
@@ -49,21 +54,40 @@ export const FromColumnsAndData: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDataStoreIntegration: Story = {
|
||||
name: 'Table with datastore integration',
|
||||
args: {
|
||||
datastoreConfig: {
|
||||
dataStoreURI: `https://www.civicdata.com/api/action/datastore_search?resource_id=46ec0807-31ff-497f-bfa0-f31c796cdee8`,
|
||||
dataMapperFn: ({
|
||||
result,
|
||||
}: {
|
||||
result: { fields: { id }[]; records: []; total: number };
|
||||
}) => {
|
||||
return {
|
||||
data: result.records,
|
||||
cols: result.fields.map((x) => ({ key: x.id, name: x.id })),
|
||||
total: result.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FromRawCSV: Story = {
|
||||
name: "Table from raw CSV",
|
||||
name: 'Table from raw CSV',
|
||||
args: {
|
||||
csv: `
|
||||
Year,Temp Anomaly
|
||||
1850,-0.418
|
||||
2020,0.923
|
||||
`
|
||||
}
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
export const FromURL: Story = {
|
||||
name: "Table from URL",
|
||||
name: 'Table from URL',
|
||||
args: {
|
||||
url: "https://raw.githubusercontent.com/datasets/finance-vix/main/data/vix-daily.csv"
|
||||
}
|
||||
url: 'https://raw.githubusercontent.com/datasets/finance-vix/main/data/vix-daily.csv',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
|
||||
? "/" + possibleWikiLinkPermalinks[0]
|
||||
: possibleWikiLinkPermalinks[0]) ||
|
||||
"";
|
||||
const isExternal = /^https?:\/\//.test(link);
|
||||
|
||||
wikiLink.data.exists = !!matchingPermalink;
|
||||
wikiLink.data.permalink = link;
|
||||
@@ -130,7 +131,7 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
|
||||
wikiLink.data.hName = "a";
|
||||
wikiLink.data.hProperties = {
|
||||
className: classNames + " " + "transclusion",
|
||||
href: hrefTemplate(link) + headingId,
|
||||
href: hrefTemplate(link) + headingId
|
||||
};
|
||||
wikiLink.data.hChildren = [{ type: "text", value: displayName }];
|
||||
|
||||
@@ -163,7 +164,11 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
|
||||
wikiLink.data.hProperties = {
|
||||
className: classNames,
|
||||
href: hrefTemplate(link) + headingId,
|
||||
target: "_blank",
|
||||
};
|
||||
if(isExternal){
|
||||
wikiLink.data.hProperties.target = "_blank"; // Open in a new tab
|
||||
}
|
||||
wikiLink.data.hChildren = [{ type: "text", value: displayName }];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,9 @@ function html(opts: HtmlOptions = {}) {
|
||||
: possibleWikiLinkPermalinks[0]) ||
|
||||
"";
|
||||
|
||||
const isExternal = /^https?:\/\//.test(link);
|
||||
const openInNewTab = isExternal ? 'target="_blank"' : '';
|
||||
|
||||
// remove leading # if the target is a heading on the same page
|
||||
const displayName = alias || target.replace(/^#/, "");
|
||||
// replace spaces with dashes and lowercase headings
|
||||
|
||||
@@ -331,4 +331,14 @@ describe("micromark-extension-wiki-link", () => {
|
||||
expect(serialized).toBe(`<p><a href="li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô-íã_a(n)d_underline!:ª%@'*º$-°~./\\" class="internal new">li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#LI NK-W(i)th-àcèô íã_a(n)d_uNdErlinE!:ª%@'*º$ °~./\\</a></p>`);
|
||||
});
|
||||
})
|
||||
|
||||
describe("External links", () => {
|
||||
test("parses an external link that opens in a new tab", () => {
|
||||
const serialized = micromark("[google](https://www.google.com/)", "ascii", {
|
||||
extensions: [syntax()],
|
||||
htmlExtensions: [html() as any],
|
||||
});
|
||||
expect(serialized).toBe(`<p><a href="https://www.google.com/" target="_blank">google</a></p>`);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ const config = {
|
||||
},
|
||||
},
|
||||
github: 'https://github.com/datopian/portaljs',
|
||||
discord: 'https://discord.gg/EeyfGrGu4U',
|
||||
discord: 'https://discord.gg/xfFDMPU9dC',
|
||||
tableOfContents: true,
|
||||
analytics: 'G-96GWZHMH57',
|
||||
// editLinkShow: true,
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../dist/out-tsc/tools",
|
||||
"rootDir": ".",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": ["node"],
|
||||
"importHelpers": false
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user