[#812,package][xl]: add Table component and story for it

This commit is contained in:
deme 2023-05-01 18:56:22 -03:00
parent 169a92d313
commit 016f3e20e9
15 changed files with 2989 additions and 1264 deletions

View File

@ -0,0 +1,16 @@
{
"sourceType": "unambiguous",
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": 100
}
}
],
"@babel/preset-typescript",
"@babel/preset-react"
],
"plugins": []
}

View File

@ -1,8 +1,27 @@
module.exports = {
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
typescript: {
check: true, // type-check stories during Storybook build
}
},
framework: {
name: '@storybook/react-webpack5',
options: {},
},
docs: {
autodocs: true,
},
};

View File

@ -1,3 +1,5 @@
import '../src/tailwind.css';
// https://storybook.js.org/docs/react/writing-stories/parameters#global-parameters
export const parameters = {
// https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args

View File

@ -12,14 +12,15 @@
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"build": "tsdx build && yarn build-tailwind",
"test": "tsdx test --passWithNoTests",
"lint": "tsdx lint",
"prepare": "tsdx build",
"size": "size-limit",
"analyze": "size-limit --why",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/tailwind.css --minify"
},
"peerDependencies": {
"react": ">=16"
@ -36,7 +37,7 @@
"trailingComma": "es5"
},
"name": "components",
"author": "joaommdemenech@gmail.com",
"author": "Datopian",
"module": "dist/components.esm.js",
"size-limit": [
{
@ -49,22 +50,34 @@
}
],
"devDependencies": {
"papaparse": "^5.4.1",
"@tanstack/react-table": "^8.8.5",
"@heroicons/react": "^2.0.17",
"@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",
"@storybook/addon-essentials": "^7.0.7",
"@storybook/addon-info": "^5.3.21",
"@storybook/addon-links": "^7.0.7",
"@storybook/addon-postcss": "^2.0.0",
"@storybook/addons": "^7.0.7",
"@storybook/cli": "^7.0.7",
"@storybook/react": "^7.0.7",
"@storybook/react-webpack5": "^7.0.7",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.2",
"husky": "^8.0.3",
"postcss": "^8.4.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-is": "^18.2.0",
"size-limit": "^8.2.4",
"storybook": "^7.0.7",
"tailwindcss": "^3.3.2",
"tsdx": "^0.14.1",
"tslib": "^2.5.0",
"typescript": "^5.0.4"

View File

@ -0,0 +1,7 @@
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View 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;

View 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);
};

View File

@ -1,15 +1 @@
import React, { FC, HTMLAttributes, ReactChild } from 'react';
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>;
};
export * from './Table';

View File

@ -0,0 +1,5 @@
export default async function loadData(url: string) {
const response = await fetch(url)
const data = await response.text()
return data
}

View 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;

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View 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 = {};

View File

@ -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 = {};

View 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