Compare commits

..

15 Commits

Author SHA1 Message Date
mohamedsalem401
806bc89e8c Open external links in a new tab 2024-02-29 13:52:36 +02:00
Rufus Pollock
ed9ac2c263 Delete tools/tsconfig.tools.json
Remove as tools is empty folder.
2024-02-28 16:49:24 +01:00
Rufus Pollock
42c72e5afd Delete tools/generators/.gitkeep
empty directory
2024-02-28 16:48:34 +01:00
Rufus Pollock
9e1a324fa1 [examples/fivethirtyeight][xs]: link to blog post we wrote about it. 2024-02-13 13:31:39 +01:00
Rufus Pollock
90178af8f2 [examples/fivethirtyeight][s]: add link to demo site to README. 2024-02-13 13:30:57 +01:00
Rufus Pollock
00e61e104c [site/config][xs]: change discord server to datahub. 2024-02-09 10:27:06 +01:00
Leonardo Yuri Farias
f7f03fddca Merge pull request #1085 from datopian/changeset-release/main
Version Packages
2024-01-31 13:20:53 -03:00
github-actions[bot]
0891dfde2d Version Packages 2024-01-31 09:31:04 +00:00
Anuar Ustayev (aka Anu)
c904e3731b Merge pull request #1083 from datopian/feature/table-with-integration-with-datastore-api
Created integration with datastore api for table component
2024-01-31 15:28:17 +06:00
Gutts-n
86a2945ee6 Created integration with datastore api for table component 2024-01-29 14:07:42 -03:00
Leonardo Yuri Farias
09daa98b28 Merge pull request #1082 from datopian/changeset-release/main
Version Packages
2024-01-25 16:49:22 -03:00
github-actions[bot]
b511c9f71b Version Packages 2024-01-25 19:48:44 +00:00
Leonardo Yuri Farias
464cda6db8 Merge pull request #1081 from datopian/fix/changed-the-download-behavior
Fixed error to remove anchor from document
2024-01-25 16:45:58 -03:00
Gutts-n
2bbf313489 Fixed error to remove anchor from document 2024-01-25 16:45:39 -03:00
Gutts-n
c26b76368d Fixed error to remove anchor from document 2024-01-25 16:43:47 -03:00
12 changed files with 268 additions and 57 deletions

View File

@@ -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:

View File

@@ -1,5 +1,17 @@
# @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

View File

@@ -1,6 +1,6 @@
{
"name": "@portaljs/components",
"version": "0.5.8",
"version": "0.5.10",
"type": "module",
"description": "https://portaljs.org",
"keywords": [

View File

@@ -137,7 +137,7 @@ export function BucketViewer({
a.download = data.fileName;
document.body.appendChild(a);
a.click();
document.removeChild(a);
document.body.removeChild(a);
}}
key={i}
onMouseEnter={() => setShowDownloadComponentOnLine(i)}

View File

@@ -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 />

View File

@@ -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',
},
};

View File

@@ -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 }];
}
}

View File

@@ -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

View File

@@ -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>`);
});
})
});

View File

@@ -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,

View File

@@ -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"]
}