[#812,package][xl]: add Table component and story for it
This commit is contained in:
parent
169a92d313
commit
016f3e20e9
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 = {
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
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 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';
|
||||
|
||||
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
Loading…
x
Reference in New Issue
Block a user