[#812,package][xl]: add Table component and story for it
This commit is contained in:
16
packages/components/.babelrc.json
Normal file
16
packages/components/.babelrc.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"sourceType": "unambiguous",
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"chrome": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
"@babel/preset-react"
|
||||||
|
],
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
@@ -1,8 +1,27 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'],
|
stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'],
|
||||||
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
|
addons: [
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
{
|
||||||
|
name: '@storybook/addon-postcss',
|
||||||
|
options: {
|
||||||
|
postcssLoaderOptions: {
|
||||||
|
implementation: require('postcss'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
// https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration
|
// https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration
|
||||||
typescript: {
|
typescript: {
|
||||||
check: true, // type-check stories during Storybook build
|
check: true, // type-check stories during Storybook build
|
||||||
}
|
},
|
||||||
|
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-webpack5',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import '../src/tailwind.css';
|
||||||
|
|
||||||
// https://storybook.js.org/docs/react/writing-stories/parameters#global-parameters
|
// https://storybook.js.org/docs/react/writing-stories/parameters#global-parameters
|
||||||
export const parameters = {
|
export const parameters = {
|
||||||
// https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args
|
// https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args
|
||||||
|
|||||||
@@ -12,14 +12,15 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsdx watch",
|
"start": "tsdx watch",
|
||||||
"build": "tsdx build",
|
"build": "tsdx build && yarn build-tailwind",
|
||||||
"test": "tsdx test --passWithNoTests",
|
"test": "tsdx test --passWithNoTests",
|
||||||
"lint": "tsdx lint",
|
"lint": "tsdx lint",
|
||||||
"prepare": "tsdx build",
|
"prepare": "tsdx build",
|
||||||
"size": "size-limit",
|
"size": "size-limit",
|
||||||
"analyze": "size-limit --why",
|
"analyze": "size-limit --why",
|
||||||
"storybook": "start-storybook -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "build-storybook"
|
"build-storybook": "storybook build",
|
||||||
|
"build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/tailwind.css --minify"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=16"
|
"react": ">=16"
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
"trailingComma": "es5"
|
"trailingComma": "es5"
|
||||||
},
|
},
|
||||||
"name": "components",
|
"name": "components",
|
||||||
"author": "joaommdemenech@gmail.com",
|
"author": "Datopian",
|
||||||
"module": "dist/components.esm.js",
|
"module": "dist/components.esm.js",
|
||||||
"size-limit": [
|
"size-limit": [
|
||||||
{
|
{
|
||||||
@@ -49,22 +50,34 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"papaparse": "^5.4.1",
|
||||||
|
"@tanstack/react-table": "^8.8.5",
|
||||||
|
"@heroicons/react": "^2.0.17",
|
||||||
"@babel/core": "^7.21.5",
|
"@babel/core": "^7.21.5",
|
||||||
|
"@babel/preset-env": "^7.21.5",
|
||||||
|
"@babel/preset-react": "^7.18.6",
|
||||||
|
"@babel/preset-typescript": "^7.21.5",
|
||||||
"@size-limit/preset-small-lib": "^8.2.4",
|
"@size-limit/preset-small-lib": "^8.2.4",
|
||||||
"@storybook/addon-essentials": "^7.0.7",
|
"@storybook/addon-essentials": "^7.0.7",
|
||||||
"@storybook/addon-info": "^5.3.21",
|
"@storybook/addon-info": "^5.3.21",
|
||||||
"@storybook/addon-links": "^7.0.7",
|
"@storybook/addon-links": "^7.0.7",
|
||||||
|
"@storybook/addon-postcss": "^2.0.0",
|
||||||
"@storybook/addons": "^7.0.7",
|
"@storybook/addons": "^7.0.7",
|
||||||
"@storybook/cli": "^7.0.7",
|
"@storybook/cli": "^7.0.7",
|
||||||
"@storybook/react": "^7.0.7",
|
"@storybook/react": "^7.0.7",
|
||||||
|
"@storybook/react-webpack5": "^7.0.7",
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.2.1",
|
"@types/react-dom": "^18.2.1",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
"babel-loader": "^9.1.2",
|
"babel-loader": "^9.1.2",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-is": "^18.2.0",
|
"react-is": "^18.2.0",
|
||||||
"size-limit": "^8.2.4",
|
"size-limit": "^8.2.4",
|
||||||
|
"storybook": "^7.0.7",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
"tsdx": "^0.14.1",
|
"tsdx": "^0.14.1",
|
||||||
"tslib": "^2.5.0",
|
"tslib": "^2.5.0",
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4"
|
||||||
|
|||||||
7
packages/components/postcss.config.js
Normal file
7
packages/components/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// postcss.config.js
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
32
packages/components/src/DebouncedInput.tsx
Normal file
32
packages/components/src/DebouncedInput.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const DebouncedInput = ({
|
||||||
|
value: initialValue,
|
||||||
|
onChange,
|
||||||
|
debounce = 500,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(initialValue);
|
||||||
|
}, [initialValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
onChange(value);
|
||||||
|
}, debounce);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DebouncedInput;
|
||||||
192
packages/components/src/Table.tsx
Normal file
192
packages/components/src/Table.tsx
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import {
|
||||||
|
createColumnHelper,
|
||||||
|
FilterFn,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowDownIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
ChevronDoubleLeftIcon,
|
||||||
|
ChevronDoubleRightIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import parseCsv from "./lib/parseCsv";
|
||||||
|
import DebouncedInput from "./DebouncedInput";
|
||||||
|
import loadData from "./lib/loadData";
|
||||||
|
|
||||||
|
export interface TableProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Table = ({
|
||||||
|
data: ogData = [],
|
||||||
|
cols: ogCols = [],
|
||||||
|
csv = "",
|
||||||
|
url = "",
|
||||||
|
fullWidth = false,
|
||||||
|
}) => {
|
||||||
|
if (csv) {
|
||||||
|
const out = parseCsv(csv);
|
||||||
|
ogData = out.rows;
|
||||||
|
ogCols = out.fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, setData] = React.useState(ogData);
|
||||||
|
const [cols, setCols] = React.useState(ogCols);
|
||||||
|
const [error, setError] = React.useState(""); // TODO: add error handling
|
||||||
|
|
||||||
|
const tableCols = useMemo(() => {
|
||||||
|
const columnHelper = createColumnHelper();
|
||||||
|
return cols.map((c) =>
|
||||||
|
columnHelper.accessor(c.key, {
|
||||||
|
header: () => c.name,
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [data, cols]);
|
||||||
|
|
||||||
|
const [globalFilter, setGlobalFilter] = useState("");
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns: tableCols,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
state: {
|
||||||
|
globalFilter,
|
||||||
|
},
|
||||||
|
globalFilterFn: globalFilterFn,
|
||||||
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (url) {
|
||||||
|
loadData(url).then((data) => {
|
||||||
|
const { rows, fields } = parseCsv(data);
|
||||||
|
setData(rows);
|
||||||
|
setCols(fields);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${fullWidth ? "w-[90vw] ml-[calc(50%-45vw)]" : "w-full"}`}>
|
||||||
|
<DebouncedInput
|
||||||
|
value={globalFilter ?? ""}
|
||||||
|
onChange={(value) => setGlobalFilter(String(value))}
|
||||||
|
className="p-2 text-sm shadow border border-block"
|
||||||
|
placeholder="Search all columns..."
|
||||||
|
/>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
{table.getHeaderGroups().map((hg) => (
|
||||||
|
<tr key={hg.id}>
|
||||||
|
{hg.headers.map((h) => (
|
||||||
|
<th key={h.id}>
|
||||||
|
<div
|
||||||
|
{...{
|
||||||
|
className: h.column.getCanSort()
|
||||||
|
? "cursor-pointer select-none"
|
||||||
|
: "",
|
||||||
|
onClick: h.column.getToggleSortingHandler(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flexRender(h.column.columnDef.header, h.getContext())}
|
||||||
|
{{
|
||||||
|
asc: (
|
||||||
|
<ArrowUpIcon className="inline-block ml-2 h-4 w-4" />
|
||||||
|
),
|
||||||
|
desc: (
|
||||||
|
<ArrowDownIcon className="inline-block ml-2 h-4 w-4" />
|
||||||
|
),
|
||||||
|
}[h.column.getIsSorted() as string] ?? (
|
||||||
|
<div className="inline-block ml-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{table.getRowModel().rows.map((r) => (
|
||||||
|
<tr key={r.id}>
|
||||||
|
{r.getVisibleCells().map((c) => (
|
||||||
|
<td key={c.id}>
|
||||||
|
{flexRender(c.column.columnDef.cell, c.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className="flex gap-2 items-center justify-center">
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanPreviousPage() ? "opacity-25" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<ChevronDoubleLeftIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanPreviousPage() ? "opacity-25" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</button>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<div>Page</div>
|
||||||
|
<strong>
|
||||||
|
{table.getState().pagination.pageIndex + 1} of{" "}
|
||||||
|
{table.getPageCount()}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanNextPage() ? "opacity-25" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanNextPage() ? "opacity-25" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<ChevronDoubleRightIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const globalFilterFn: FilterFn<any> = (row, columnId, filterValue: string) => {
|
||||||
|
const search = filterValue.toLowerCase();
|
||||||
|
|
||||||
|
let value = row.getValue(columnId) as string;
|
||||||
|
if (typeof value === "number") value = String(value);
|
||||||
|
|
||||||
|
return value?.toLowerCase().includes(search);
|
||||||
|
};
|
||||||
|
|
||||||
@@ -1,15 +1 @@
|
|||||||
import React, { FC, HTMLAttributes, ReactChild } from 'react';
|
export * from './Table';
|
||||||
|
|
||||||
export interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
||||||
/** custom content, defaults to 'the snozzberries taste like snozzberries' */
|
|
||||||
children?: ReactChild;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Please do not use types off of a default export module or else Storybook Docs will suffer.
|
|
||||||
// see: https://github.com/storybookjs/storybook/issues/9556
|
|
||||||
/**
|
|
||||||
* A custom Thing component. Neat!
|
|
||||||
*/
|
|
||||||
export const Thing: FC<Props> = ({ children }) => {
|
|
||||||
return <div>{children || `the snozzberries taste like snozzberries`}</div>;
|
|
||||||
};
|
|
||||||
|
|||||||
5
packages/components/src/lib/loadData.tsx
Normal file
5
packages/components/src/lib/loadData.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default async function loadData(url: string) {
|
||||||
|
const response = await fetch(url)
|
||||||
|
const data = await response.text()
|
||||||
|
return data
|
||||||
|
}
|
||||||
16
packages/components/src/lib/parseCsv.ts
Normal file
16
packages/components/src/lib/parseCsv.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import papa from "papaparse";
|
||||||
|
|
||||||
|
const parseCsv = (csv) => {
|
||||||
|
csv = csv.trim();
|
||||||
|
const rawdata = papa.parse(csv, { header: true });
|
||||||
|
const cols = rawdata.meta.fields.map((r, i) => {
|
||||||
|
return { key: r, name: r };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows: rawdata.data,
|
||||||
|
fields: cols,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default parseCsv;
|
||||||
3
packages/components/src/tailwind.css
Normal file
3
packages/components/src/tailwind.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
45
packages/components/stories/Table.stories.tsx
Normal file
45
packages/components/stories/Table.stories.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Meta, Story, StoryFn } from '@storybook/react';
|
||||||
|
import { Table, TableProps } from '../src';
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Table',
|
||||||
|
component: Table,
|
||||||
|
args: {
|
||||||
|
data: [
|
||||||
|
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
|
||||||
|
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
|
||||||
|
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
|
||||||
|
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
|
||||||
|
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
|
||||||
|
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
|
||||||
|
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
|
||||||
|
],
|
||||||
|
cols: [
|
||||||
|
{ key: 'id', name: 'ID' },
|
||||||
|
{ key: 'firstName', name: 'First name' },
|
||||||
|
{ key: 'lastName', name: 'Last name' },
|
||||||
|
{ key: 'age', name: 'Age' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
data: {
|
||||||
|
description: 'Data that will be displayed in the table',
|
||||||
|
control: 'object',
|
||||||
|
},
|
||||||
|
cols: {
|
||||||
|
description: 'Columns that are going to be displayed in the table',
|
||||||
|
control: 'object',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
const Template: StoryFn<TableProps> = (args) => <Table {...args} />;
|
||||||
|
|
||||||
|
// By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
|
||||||
|
// https://storybook.js.org/docs/react/workflows/unit-testing
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
|
||||||
|
Default.args = {};
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Meta, Story } from '@storybook/react';
|
|
||||||
import { Thing, Props } from '../src';
|
|
||||||
|
|
||||||
const meta: Meta = {
|
|
||||||
title: 'Welcome',
|
|
||||||
component: Thing,
|
|
||||||
argTypes: {
|
|
||||||
children: {
|
|
||||||
control: {
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
parameters: {
|
|
||||||
controls: { expanded: true },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
const Template: Story<Props> = args => <Thing {...args} />;
|
|
||||||
|
|
||||||
// By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
|
|
||||||
// https://storybook.js.org/docs/react/workflows/unit-testing
|
|
||||||
export const Default = Template.bind({});
|
|
||||||
|
|
||||||
Default.args = {};
|
|
||||||
10
packages/components/tailwind.config.js
Normal file
10
packages/components/tailwind.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./src/**/*.{tsx,jsx}"],
|
||||||
|
darkMode: false, // or 'media' or 'class'
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
variants: {},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user