diff --git a/package.json b/package.json index b582e9fd..ea38c7da 100755 --- a/package.json +++ b/package.json @@ -108,6 +108,8 @@ "dependencies": { "@tippyjs/react": "^4.2.5", "color-rgba": "^2.2.3", + "effector": "^21.8.12", + "effector-react": "^21.3.3", "nanoid": "^3.1.23", "react": "16.14.0", "react-colorful": "^5.2.3", diff --git a/src/woly/atoms/table/index.tsx b/src/woly/atoms/table/index.tsx index bd38a4e8..6624e50a 100644 --- a/src/woly/atoms/table/index.tsx +++ b/src/woly/atoms/table/index.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import styled, { StyledComponent } from 'styled-components'; import { Priority } from 'lib/types'; const map = (properties: { columns: number } & Priority) => ({ @@ -17,7 +17,7 @@ export const Table = styled.table.attrs(map)` display: grid; grid-template-columns: repeat(var(--local-columns), auto); gap: var(--local-gap); -`; +` as StyledComponent<'table', Record, { columns: number } & Priority>; export const Thead = styled.thead` display: contents; @@ -32,9 +32,13 @@ export const Th = styled.th` align-items: center; box-sizing: border-box; max-width: var(--local-cell-max-width); + /* TODO: Replace with box [09.08.2020] */ padding: var(--local-vertical) var(--local-horizontal); color: var(--woly-canvas-text-disabled); + font-weight: normal; + + line-height: var(--woly-line-height); background: var(--woly-shape-text-default); `; @@ -46,6 +50,7 @@ export const Td = styled.td` padding: var(--local-vertical) var(--local-horizontal); color: var(--woly-canvas-text-default); + line-height: var(--woly-line-height); background: var(--woly-shape-text-default); `; diff --git a/src/woly/atoms/table/usage.mdx b/src/woly/atoms/table/usage.mdx index 5caf6a7a..7805d267 100644 --- a/src/woly/atoms/table/usage.mdx +++ b/src/woly/atoms/table/usage.mdx @@ -1,6 +1,11 @@ import {Playground} from 'dev/playground' import {Table, Tbody, Td, Th, Thead, Tr} from 'ui' +To create a table use Table, Thead, Tbody, Th, Tr, Td components. +To set number of columns pass `columns` prop to the Table component. + +### Example + export const tableHead = [ {id: 'id', name: 'Id'}, {id: 'firstName', name: 'First name'}, @@ -65,11 +70,6 @@ export const tableRows = [ }, ]; -To create a table use Table, Thead, Tbody, Th, Tr, Td components. -To set number of columns pass `columns` prop to the Table component. - -### Example - diff --git a/src/woly/molecules/data-table/filter.tsx b/src/woly/molecules/data-table/filter.tsx new file mode 100644 index 00000000..2667779d --- /dev/null +++ b/src/woly/molecules/data-table/filter.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import styled from 'styled-components'; +import { IconArrowDown } from 'static/icons'; +import { box } from 'ui/elements/box'; + +import { Button, Checkbox, ListContainer, Popover, Surface } from '../../index'; + +interface Option { + name: string; + value: string; +} + +interface FilterProps { + title: React.ReactNode | string; + options: Option[]; + value: string[]; + onChange: (value: string[]) => void; +} + +export const Filter: React.FC = ({ + title, + options, + value: checkedValues, + onChange, +}) => { + const [isOpen, setOpen] = React.useReducer((is) => !is, false); + + const getUpdatedValue = (value: string) => { + if (checkedValues.includes(value)) { + // remove + return checkedValues.filter((item) => item !== value); + } + + // add + return checkedValues.concat(value); + }; + + const createCheckHandler = (item: string) => { + return () => onChange(getUpdatedValue(item)); + }; + + if (options.length === 0) { + console.log('No options are passed to filter'); + return null; + } + + return ( + + + + {options.map(({ name, value }) => ( + + ))} + + + } + > + +
{title}
+
+ +
+
+
+
+ ); +}; + +const Dropdown = styled(Surface)` + position: absolute; + + display: flex; + flex-direction: column; + width: 100%; + + font-weight: normal; +`; + +const FilterBlock = styled.div` + position: relative; +`; + +const FilterButton = styled.div` + ${box} + + display: flex; + align-items: center; + box-sizing: border-box; + + background: var(--woly-shape-text-default); + + border: var(--woly-border-width) solid var(--woly-canvas-default); + + cursor: pointer; + + [data-icon] { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + svg > path { + fill: var(--woly-canvas-text-disabled); + } + } +`; diff --git a/src/woly/molecules/data-table/index.tsx b/src/woly/molecules/data-table/index.tsx new file mode 100644 index 00000000..cddcb3fc --- /dev/null +++ b/src/woly/molecules/data-table/index.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; + +import { CellProps, DataTableProps, HeadGroupProps, HeadProps } from './types'; +import { Table, Tbody, Td, Th, Thead, Tr } from '../../atoms/table'; + +export function DataTable({ + rowKey, + columns, + placeholder = '----', + priority = 'secondary', + values, + ...rest +}: DataTableProps) { + return ( + // eslint-disable-next-line react/jsx-props-no-spreading +
+ + + {values.map((row) => ( + + {columns.map((column) => { + const Cell = column.cell || DefaultCell; + + return ( + + ); + })} + + ))} + +
+ +
+ ); +} + +function DefaultCell({ value, placeholder }: CellProps) { + return <>{value || placeholder}; +} + +function DefaultHead({ title }: HeadProps) { + return <>{title}; +} + +function TableHeadGroup({ columns }: HeadGroupProps) { + return ( + + + {columns.map(({ title, property, head }) => { + const Head = head || DefaultHead; + const padding = isReactEntity(title) ? '0' : ''; + + return ( + + + + ); + })} + + + ); +} + +const isReactComponent = (value: unknown) => typeof value === 'function'; +const isReactElement = (value: unknown) => typeof value === 'object'; +const isReactEntity = (value: unknown) => isReactComponent(value) || isReactElement(value); + +export { DataTableColumn, HeadProps as DataTableHeadProps } from './types'; diff --git a/src/woly/molecules/data-table/range-cell.tsx b/src/woly/molecules/data-table/range-cell.tsx new file mode 100644 index 00000000..50582ed8 --- /dev/null +++ b/src/woly/molecules/data-table/range-cell.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +interface RangeProps { + value: { + from: number; + to: number; + }; + placeholder: string; +} + +export const RangeCell: React.FC = ({ value, placeholder }) => ( + + from {value.from || placeholder} to {value.to || placeholder} + +); diff --git a/src/woly/molecules/data-table/types.ts b/src/woly/molecules/data-table/types.ts new file mode 100644 index 00000000..80c8f339 --- /dev/null +++ b/src/woly/molecules/data-table/types.ts @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { Priority } from 'lib/types'; + +export type CellType = { + [key in TRowKey]: string; +} & + Record; + +export interface HeadProps { + title: React.ReactNode | string; +} + +export interface CellProps { + placeholder?: React.ReactNode | string; + value: TValue; +} + +export interface DataTableColumn { + title: React.ReactNode | string; + property: string; + head?: React.FC; + cell?: React.FC>; + placeholder?: React.ReactNode | string; +} + +export type DataTableProps< + TValue, + TRowKey extends string +> = React.HTMLAttributes & + Priority & { + rowKey: TRowKey; + columns: Array>; + placeholder?: React.ReactNode | string; + values: Array>; + }; + +export interface HeadGroupProps { + columns: Array>; +} diff --git a/src/woly/molecules/data-table/usage.mdx b/src/woly/molecules/data-table/usage.mdx new file mode 100644 index 00000000..7b04ff10 --- /dev/null +++ b/src/woly/molecules/data-table/usage.mdx @@ -0,0 +1,200 @@ +### Example + +```tsx playground +import { DataTable, DataTableHeadProps } from 'woly' +import { createStore, createEvent, forward, combine } from 'effector' +import { useStore } from 'effector-react' +import { Filter } from './filter' +import { RangeCell } from './range-cell' + +const bankNames = [ + { + value: 'ВТБ', + name: 'ВТБ', + }, + { + value: 'Тинькофф', + name: 'Тинькофф', + }, + { + value: 'Альфа-банк', + name: 'Альфа', + }, + { + value: 'Открытие', + name: 'Открытие', + }, +]; + +const paymentSystems = [ + { + value: 'Visa/Mastercard', + name: 'Visa/Mastercard', + }, + { + value: 'МИР', + name: 'МИР', + }, +]; + +const $bankFilter = createStore([]) +const bankFilterUpdated = createEvent() +forward({ from: bankFilterUpdated, to: $bankFilter }) + +const $paymentSystemFilter = createStore([]) +const paymentSystemFilterUpdated = createEvent() +forward({ from: paymentSystemFilterUpdated, to: $paymentSystemFilter }) + +const columns = [ + { + title: 'ID Дебет', + property: 'id-debet', + }, + { + title: 'ID Кредит', + property: 'id-credit', + }, + { + title: 'Название банка', + property: 'bank-name', + head: ({ title }: DataTableHeadProps) => ( + + ), + }, + { + title: 'Платежная система', + property: 'payment-system', + head: ({ title }: DataTableHeadProps) => ( + + ), + }, + { + title: 'Диапазон', + property: 'range', + cell: RangeCell, + }, +]; + +const values = [ + { + id: 1, + 'id-debet': 798172, + 'id-credit': null, + 'bank-name': 'ВТБ', + 'payment-system': 'Visa/Mastercard', + range: { + from: 1, + to: 4, + }, + }, + { + id: 2, + 'id-debet': 798173, + 'id-credit': null, + 'bank-name': 'Альфа-банк', + 'payment-system': 'Visa/Mastercard', + range: { + from: 4, + to: 6, + }, + }, + { + id: 3, + 'id-debet': 798174, + 'id-credit': null, + 'bank-name': 'ВТБ', + 'payment-system': 'МИР', + range: { + from: 4, + to: 6, + }, + }, + { + id: 4, + 'id-debet': 798175, + 'id-credit': null, + 'bank-name': 'Тинькофф', + 'payment-system': 'Visa/Mastercard', + range: { + from: 3, + to: 6, + }, + }, + { + id: 5, + 'id-debet': 798176, + 'id-credit': null, + 'bank-name': 'Открытие', + 'payment-system': 'МИР', + range: { + from: 4, + to: 6, + }, + }, + { + id: 6, + 'id-debet': 798177, + 'id-credit': null, + 'bank-name': 'Альфа-банк', + 'payment-system': 'МИР', + range: { + from: 4, + to: 9, + }, + }, +]; + +const $filteredValues = combine( + $bankFilter, + $paymentSystemFilter, + (banks, paymentSystems) => { + return values.filter(value => { + const bankMatches = banks.length === 0 || banks.includes(value['bank-name']) + const paymentSystemMatches = paymentSystems.length === 0 || paymentSystems.includes(value['payment-system']) + console.log(banks, value['bank-name']) + return bankMatches && paymentSystemMatches + }) + } +) + +export function Example() { + const values = useStore($filteredValues) + + return + + + + +} +``` + +### Components + +| Name | Description | +| ----------- | ------------------- | +| `DataTable` | DataTable component | + +### DataTable Props + +| Name | Type | Default | Description | +| ------------- | ---------------------------------- | ------------- | -------------------------------------------- | +| `columns` | `Array` | | Table head | +| `placeholder` | `React.ReactNode ӏ string` | `'----'` | String that is displayed when value is empty | +| `values` | `Array>` | | Table content | +| `priority` | `string` | `'secondary'` | Priority prop to style DataTable component | + +### HeadProps + +| Name | Type | Default | Description | +| ------- | -------------------------- | ------- | ------------------------------------------------- | +| `title` | `React.ReactNode ӏ string` | | A column name passed via `title` of `ColumnProps` | + +### ColumnProps + +| Name | Type | Default | Description | +| ------------- | -------------------------- | ------- | ------------------------------------------------------------- | +| `title` | `React.ReactNode ӏ string` | | A column name. Can be a simple string or a react node | +| `property` | `string` | | Unique property of column to connect head with value in a row | +| `head` | `React.FC` | | FC to render head | +| `cell` | `React.FC` | | FC to render cell | +| `placeholder` | `React.ReactNode ӏ string` | | A column-specific placeholder | diff --git a/src/woly/molecules/index.ts b/src/woly/molecules/index.ts index 853933fa..aa986f5d 100644 --- a/src/woly/molecules/index.ts +++ b/src/woly/molecules/index.ts @@ -1,5 +1,6 @@ export { Accordion } from './accordion'; export { Checkbox } from './checkbox'; +export { DataTable, DataTableColumn, HeadProps as DataTableHeadProps } from './data-table'; export { Field } from './field'; export { InputPassword } from './input-password'; export { Notification } from './notification'; diff --git a/yarn.lock b/yarn.lock index e4984e92..778d573f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7112,6 +7112,16 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= +effector-react@^21.3.3: + version "21.3.3" + resolved "https://registry.yarnpkg.com/effector-react/-/effector-react-21.3.3.tgz#e17c095ca7b6d63af3528b1b5ebf94c3de0149db" + integrity sha512-pk+eZQcoI0q8sMtxAvm/lWsZutHW5t0aQBt0edy1XTvjKIhgimFhpxUqJtdtr0asz+9OCN0iG6FYa+Iqo6qpkw== + +effector@^21.8.12: + version "21.8.12" + resolved "https://registry.yarnpkg.com/effector/-/effector-21.8.12.tgz#075ba0e17e41386bef271567a259585f407067ae" + integrity sha512-Xlw+qvPlfiF0qOHwRQ3RzS5/TtbfpCZ88ucLqG6o8algEV0nxI+Rd8fyJObDljfqQH0+2ByInawu731IXz5RCA== + electron-to-chromium@^1.3.564, electron-to-chromium@^1.3.649: version "1.3.752" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz#0728587f1b9b970ec9ffad932496429aef750d09" @@ -7423,7 +7433,7 @@ eslint-import-resolver-alias@^1.1.2: resolved "https://registry.yarnpkg.com/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz#297062890e31e4d6651eb5eba9534e1f6e68fc97" integrity sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w== -eslint-import-resolver-node@^0.3.3, eslint-import-resolver-node@^0.3.4: +eslint-import-resolver-node@^0.3.3: version "0.3.4" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== @@ -7431,6 +7441,14 @@ eslint-import-resolver-node@^0.3.3, eslint-import-resolver-node@^0.3.4: debug "^2.6.9" resolve "^1.13.1" +eslint-import-resolver-node@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.5.tgz#939bbb0f74e179e757ca87f7a4a890dabed18ac4" + integrity sha512-XMoPKjSpXbkeJ7ZZ9icLnJMTY5Mc1kZbCakHquaFsXPpyWOwK0TK6CODO+0ca54UoM9LKOxyUNnoVZRl8TeaAg== + dependencies: + debug "^3.2.7" + resolve "^1.20.0" + eslint-import-resolver-typescript@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.3.0.tgz#0870988098bc6c6419c87705e6b42bee89425445" @@ -7442,7 +7460,7 @@ eslint-import-resolver-typescript@2.3.0: resolve "^1.17.0" tsconfig-paths "^3.9.0" -eslint-module-utils@^2.6.0, eslint-module-utils@^2.6.1: +eslint-module-utils@^2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.1.tgz#b51be1e473dd0de1c5ea638e22429c2490ea8233" integrity sha512-ZXI9B8cxAJIH4nfkhTwcRTEAnrVfobYqwjWy/QMCZ8rHkZHFjf9yO4BzpiF9kCSfNlMG54eKigISHpX0+AaT4A== @@ -7450,6 +7468,14 @@ eslint-module-utils@^2.6.0, eslint-module-utils@^2.6.1: debug "^3.2.7" pkg-dir "^2.0.0" +eslint-module-utils@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz#94e5540dd15fe1522e8ffa3ec8db3b7fa7e7a534" + integrity sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q== + dependencies: + debug "^3.2.7" + pkg-dir "^2.0.0" + eslint-plugin-flowtype@^5.3.1: version "5.7.2" resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.7.2.tgz#482a42fe5d15ee614652ed256d37543d584d7bc0"