diff --git a/package.json b/package.json index 88307c2..c310dc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-restful-table", - "version": "0.3.0", + "version": "0.4.0", "license": "LGPL-3.0", "author": "shiy2008@gmail.com", "description": "A super Table component for CRUD operation, which is based on MobX RESTful & React.", @@ -28,22 +28,29 @@ "dependencies": { "@swc/helpers": "^0.4.14", "classnames": "^2.3.2", - "mobx-i18n": "^0.3.3", + "lodash": "^4.17.21", + "mobx-i18n": "^0.3.6", "mobx-react": "^6.3.1", - "mobx-restful": "^0.6.0-rc.18", - "react": ">=16 <18", - "react-bootstrap": "^2.6.0", + "mobx-restful": "^0.6.0-rc.19", + "react-bootstrap": "^2.7.0", "regenerator-runtime": "^0.13.11", "web-utility": "^3.9.9" }, + "peerDependencies": { + "mobx": ">=4 <6", + "react": ">=16 <18" + }, "devDependencies": { "@parcel/packager-ts": "~2.6.2", "@parcel/transformer-typescript-types": "~2.6.2", + "@types/lodash": "^4.14.191", "@types/react": "^17.0.52", "husky": "^8.0.2", "lint-staged": "^13.1.0", + "mobx": "^5.15.7", "parcel": "~2.6.2", - "prettier": "^2.8.0", + "prettier": "^2.8.1", + "react": "^17.0.2", "typedoc": "^0.23.21", "typedoc-plugin-mdn-links": "^2.0.0", "typescript": "~4.7.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62f7cb4..ff34808 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,17 +4,20 @@ specifiers: '@parcel/packager-ts': ~2.6.2 '@parcel/transformer-typescript-types': ~2.6.2 '@swc/helpers': ^0.4.14 + '@types/lodash': ^4.14.191 '@types/react': ^17.0.52 classnames: ^2.3.2 husky: ^8.0.2 lint-staged: ^13.1.0 - mobx-i18n: ^0.3.3 + lodash: ^4.17.21 + mobx: ^5.15.7 + mobx-i18n: ^0.3.6 mobx-react: ^6.3.1 - mobx-restful: ^0.6.0-rc.18 + mobx-restful: ^0.6.0-rc.19 parcel: ~2.6.2 - prettier: ^2.8.0 - react: '>=16 <18' - react-bootstrap: ^2.6.0 + prettier: ^2.8.1 + react: ^17.0.2 + react-bootstrap: ^2.7.0 regenerator-runtime: ^0.13.11 typedoc: ^0.23.21 typedoc-plugin-mdn-links: ^2.0.0 @@ -24,22 +27,25 @@ specifiers: dependencies: '@swc/helpers': 0.4.14 classnames: 2.3.2 - mobx-i18n: 0.3.3 - mobx-react: 6.3.1_react@17.0.2 - mobx-restful: 0.6.0-rc.18_typescript@4.7.4 - react: 17.0.2 - react-bootstrap: 2.6.0_q5o373oqrklnndq2vhekyuzhxi + lodash: 4.17.21 + mobx-i18n: 0.3.6_mobx@5.15.7 + mobx-react: 6.3.1_mobx@5.15.7+react@17.0.2 + mobx-restful: 0.6.0-rc.19_dss5dgebhlll4wuvbmbwonwy4e + react-bootstrap: 2.7.0_q5o373oqrklnndq2vhekyuzhxi regenerator-runtime: 0.13.11 web-utility: 3.9.9_typescript@4.7.4 devDependencies: '@parcel/packager-ts': 2.6.2 '@parcel/transformer-typescript-types': 2.6.2_typescript@4.7.4 + '@types/lodash': 4.14.191 '@types/react': 17.0.52 husky: 8.0.2 lint-staged: 13.1.0 + mobx: 5.15.7 parcel: 2.6.2 - prettier: 2.8.0 + prettier: 2.8.1 + react: 17.0.2 typedoc: 0.23.21_typescript@4.7.4 typedoc-plugin-mdn-links: 2.0.0_typedoc@0.23.21 typescript: 4.7.4 @@ -973,6 +979,10 @@ packages: engines: {node: '>=10.13.0'} dev: true + /@types/lodash/4.14.191: + resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} + dev: true + /@types/parse-json/4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} dev: true @@ -1737,6 +1747,10 @@ packages: '@lmdb/lmdb-win32-x64': 2.5.2 dev: true + /lodash/4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + /log-update/4.0.0: resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} engines: {node: '>=10'} @@ -1752,7 +1766,6 @@ packages: hasBin: true dependencies: js-tokens: 4.0.0 - dev: false /lunr/2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} @@ -1797,16 +1810,17 @@ packages: brace-expansion: 2.0.1 dev: true - /mobx-i18n/0.3.3: - resolution: {integrity: sha512-o2HPDQO1464CMKfenBXHU1foyPxCkKnbnXYrYCXdtzX+8aFwkAa93MxDyXR9zXT0AiZt7jZikO2b0S8kKu0I0Q==} + /mobx-i18n/0.3.6_mobx@5.15.7: + resolution: {integrity: sha512-hfa+TUaIpgGAGIJ0tq6WOHYNTrTHPt6bUHbAwNxbyiP+puCnYAA8EnKyBLXALUi+cvTB4jtv6MgJueoPjI96rQ==} peerDependencies: mobx: '>=4 <6' dependencies: '@swc/helpers': 0.4.14 + mobx: 5.15.7 regenerator-runtime: 0.13.11 dev: false - /mobx-react-lite/2.2.2_react@17.0.2: + /mobx-react-lite/2.2.2_mobx@5.15.7+react@17.0.2: resolution: {integrity: sha512-2SlXALHIkyUPDsV4VTKVR9DW7K3Ksh1aaIv3NrNJygTbhXe2A9GrcKHZ2ovIiOp/BXilOcTYemfHHZubP431dg==} peerDependencies: mobx: ^4.0.0 || ^5.0.0 @@ -1819,30 +1833,33 @@ packages: react-native: optional: true dependencies: + mobx: 5.15.7 react: 17.0.2 dev: false - /mobx-react/6.3.1_react@17.0.2: + /mobx-react/6.3.1_mobx@5.15.7+react@17.0.2: resolution: {integrity: sha512-IOxdJGnRSNSJrL2uGpWO5w9JH5q5HoxEqwOF4gye1gmZYdjoYkkMzSGMDnRCUpN/BNzZcFoMdHXrjvkwO7KgaQ==} peerDependencies: mobx: ^5.15.4 || ^4.15.4 react: ^16.8.0 || 16.9.0-alpha.0 dependencies: - mobx-react-lite: 2.2.2_react@17.0.2 + mobx: 5.15.7 + mobx-react-lite: 2.2.2_mobx@5.15.7+react@17.0.2 react: 17.0.2 transitivePeerDependencies: - react-dom - react-native dev: false - /mobx-restful/0.6.0-rc.18_typescript@4.7.4: - resolution: {integrity: sha512-Tawww9r8Fb9saT+y7/Xun0RsoyXgiVfUFA7z7PsHpGI8agYx0h9N2LML47Zh3LT5bsZgkuyL01lOOSbXARpTzQ==} + /mobx-restful/0.6.0-rc.19_dss5dgebhlll4wuvbmbwonwy4e: + resolution: {integrity: sha512-88o3fJmSZRoJNpIZWMgum/DgKFxDxYRZhrEr6ovhZOG+346Ahl54x41CxDHQcu7NRFwpxyMwO0j1Ixjw9DSK9A==} peerDependencies: mobx: '>=4 <6' dependencies: '@swc/helpers': 0.4.14 class-validator: 0.13.2 koajax: 0.8.3_typescript@4.7.4 + mobx: 5.15.7 reflect-metadata: 0.1.13 regenerator-runtime: 0.13.11 web-utility: 3.9.9_typescript@4.7.4 @@ -1851,6 +1868,9 @@ packages: - typescript dev: false + /mobx/5.15.7: + resolution: {integrity: sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==} + /ms/2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true @@ -1924,7 +1944,6 @@ packages: /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - dev: false /object-inspect/1.12.2: resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} @@ -2067,8 +2086,8 @@ packages: posthtml-render: 3.0.0 dev: true - /prettier/2.8.0: - resolution: {integrity: sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==} + /prettier/2.8.1: + resolution: {integrity: sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==} engines: {node: '>=10.13.0'} hasBin: true dev: true @@ -2091,8 +2110,8 @@ packages: react-is: 16.13.1 dev: false - /react-bootstrap/2.6.0_q5o373oqrklnndq2vhekyuzhxi: - resolution: {integrity: sha512-WnDgN6PR8WZKo2Og5J8EafFi4BsABjc96lNuMNfksrgiPDCw18/woWQCNhAeHFZQWTQ/PijkOrQ9ncTWwO//AA==} + /react-bootstrap/2.7.0_q5o373oqrklnndq2vhekyuzhxi: + resolution: {integrity: sha512-Jcrn6aUuRVBeSB6dzKODKZU1TONOdhAxu0IDm4Sv74SJUm98dMdhSotF2SNvFEADANoR+stV+7TK6SNX1wWu5w==} peerDependencies: '@types/react': '>=16.14.8' react: '>=16.14.0' @@ -2153,7 +2172,6 @@ packages: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 - dev: false /reflect-metadata/0.1.13: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} diff --git a/source/FormField.tsx b/source/FormField.tsx new file mode 100644 index 0000000..d79d5cb --- /dev/null +++ b/source/FormField.tsx @@ -0,0 +1,21 @@ +import { InputHTMLAttributes, FC } from 'react'; +import { FloatingLabelProps, FormControlProps, Form } from 'react-bootstrap'; + +export type FormFieldProps = InputHTMLAttributes & + FormControlProps & + Pick; + +export const FormField: FC = ({ + className, + style, + label, + id, + name, + ...controlProps +}) => ( + + + +); + +FormField.displayName = 'FormField'; diff --git a/source/Pager.tsx b/source/Pager.tsx index cce724c..1a19740 100644 --- a/source/Pager.tsx +++ b/source/Pager.tsx @@ -1,36 +1,77 @@ import { ListModel } from 'mobx-restful'; import { FC } from 'react'; -import { Pagination } from 'react-bootstrap'; +import { Pagination, Form } from 'react-bootstrap'; -export interface PagerProps - extends Pick, 'pageIndex' | 'pageCount'> { - onChange?: (index: number) => any; +export type PageMeta = Pick, 'pageSize' | 'pageIndex'>; + +export interface PagerProps extends PageMeta { + pageCount: number; + onChange?: (meta: PageMeta) => any; } -export const Pager: FC = ({ pageIndex, pageCount, onChange }) => ( - - {pageIndex > 1 && ( - onChange?.(1)}>1 - )} - {pageIndex > 3 && } - {pageIndex > 2 && ( - onChange?.(pageIndex - 1)}> - {pageIndex - 1} - - )} - {pageIndex} - {pageCount - pageIndex > 1 && ( - onChange?.(pageIndex + 1)}> - {pageIndex + 1} - - )} - {pageCount - pageIndex > 2 && } - {pageIndex < pageCount && ( - onChange?.(pageCount)}> - {pageCount} - - )} - +export const Pager: FC = ({ + pageSize, + pageIndex, + pageCount, + onChange, +}) => ( +
+ + input.reportValidity() && + onChange?.({ pageSize: +input.value, pageIndex }) + } + /> + x + + input.reportValidity() && + onChange?.({ pageSize, pageIndex: +input.value }) + } + /> + + {pageIndex > 1 && ( + onChange?.({ pageSize, pageIndex: 1 })}> + 1 + + )} + {pageIndex > 3 && } + {pageIndex > 2 && ( + onChange?.({ pageSize, pageIndex: pageIndex - 1 })} + > + {pageIndex - 1} + + )} + {pageIndex} + {pageCount - pageIndex > 1 && ( + onChange?.({ pageSize, pageIndex: pageIndex + 1 })} + > + {pageIndex + 1} + + )} + {pageCount - pageIndex > 2 && } + {pageIndex < pageCount && ( + onChange?.({ pageSize, pageIndex: pageCount })} + > + {pageCount} + + )} + +
); Pager.displayName = 'Pager'; diff --git a/source/RestTable.tsx b/source/RestTable.tsx index 8aabd48..1bdf453 100644 --- a/source/RestTable.tsx +++ b/source/RestTable.tsx @@ -1,5 +1,7 @@ import { isEmpty, uniqueID, formToJSON } from 'web-utility'; import classNames from 'classnames'; +import { debounce } from 'lodash'; +import { observable } from 'mobx'; import { TranslationModel } from 'mobx-i18n'; import { DataObject, IDType, ListModel } from 'mobx-restful'; import { observer } from 'mobx-react'; @@ -10,17 +12,16 @@ import { ReactNode, } from 'react'; import { - Row, - Col, Table, TableProps, Spinner, - Modal, - Form, Button, + Form, + Modal, } from 'react-bootstrap'; import { Pager } from './Pager'; +import { FormField } from './FormField'; export interface Column extends Pick, 'type'> { @@ -32,12 +33,20 @@ export interface Column export interface RestTableProps extends TableProps { editable?: boolean; + deletable?: boolean; columns: Column[]; store: ListModel; translater: TranslationModel< string, - 'create' | 'submit' | 'cancel' | 'total_x_rows' + | 'create' + | 'edit' + | 'delete' + | 'submit' + | 'cancel' + | 'total_x_rows' + | 'sure_to_delete_x' >; + onCheck?: (keys: IDType[]) => any; } @observer @@ -46,12 +55,112 @@ export class RestTable extends PureComponent< > { static displayName = 'RestTable'; + @observable + checkedKeys: IDType[] = []; + + toggleCheck(key: IDType) { + const { checkedKeys } = this; + const index = checkedKeys.indexOf(key); + + this.checkedKeys = + index < 0 + ? [...checkedKeys, key] + : [...checkedKeys.slice(0, index), ...checkedKeys.slice(index + 1)]; + + this.props.onCheck?.(this.checkedKeys); + } + + toggleCheckAll = () => { + const { store, onCheck } = this.props; + const { indexKey, currentPage } = store; + + this.checkedKeys = this.checkedKeys.length + ? [] + : currentPage.map(({ [indexKey]: ID }) => ID); + + onCheck?.(this.checkedKeys); + }; + + get columns(): Column[] { + const { checkedKeys, toggleCheckAll } = this, + { editable, deletable, columns, store, translater, onCheck } = this.props; + const { t } = translater, + { indexKey, currentPage } = store; + + return [ + onCheck && + ({ + renderHead: () => ( + + checkedKeys.includes(ID), + ) + } + ref={(input: HTMLInputElement | null) => + input && + (input.indeterminate = + !!checkedKeys.length && + checkedKeys.length < currentPage.length) + } + onClick={toggleCheckAll} + onKeyUp={({ key }) => key === ' ' && toggleCheckAll()} + /> + ), + renderBody: ({ [indexKey]: ID }) => ( + this.toggleCheck(ID)} + onKeyUp={({ key }) => key === ' ' && this.toggleCheck(ID)} + /> + ), + } as Column), + + ...columns, + + (editable || deletable) && + ({ + renderBody: data => ( + <> + {editable && ( + + )} + {deletable && ( + + )} + + ), + } as Column), + ].filter(Boolean); + } + get hasHeader() { - return this.props.columns.some(({ renderHead }) => renderHead); + return this.columns.some(({ renderHead }) => renderHead); } get hasFooter() { - return this.props.columns.some(({ renderFoot }) => renderFoot); + return this.columns.some(({ renderFoot }) => renderFoot); } componentDidMount() { @@ -64,13 +173,16 @@ export class RestTable extends PureComponent< renderTable() { const { className, - columns, + columns: _, store, translater, + editable, + deletable, + onCheck, responsive = true, ...tableProps } = this.props, - { hasHeader, hasFooter } = this; + { hasHeader, hasFooter, columns } = this; const { indexKey, downloading, currentPage } = store; return ( @@ -80,7 +192,7 @@ export class RestTable extends PureComponent< {columns.map(({ key, renderHead }, index) => ( - {key && typeof renderHead === 'function' + {typeof renderHead === 'function' ? renderHead(key) : renderHead || key} @@ -113,7 +225,7 @@ export class RestTable extends PureComponent< {columns.map(({ key, renderFoot }, index) => ( - {key && typeof renderFoot === 'function' + {typeof renderFoot === 'function' ? renderFoot(key) : renderFoot || key} @@ -137,28 +249,19 @@ export class RestTable extends PureComponent< renderInput = ({ key, type, renderHead }: Column) => { const { currentOne } = this.props.store; + const label = + typeof renderHead === 'function' ? renderHead?.(key) : renderHead || key; return ( key && ( - - - {typeof renderHead === 'function' - ? renderHead?.(key) - : renderHead || key} - - - - - + label={label} + type={type} + name={key.toString()} + defaultValue={currentOne[key]} + /> ) ); }; @@ -203,9 +306,23 @@ export class RestTable extends PureComponent< ); } + getList = debounce(({ pageIndex, pageSize }) => { + const { store } = this.props; + + if (store.downloading < 1 && !store.noMore) + store.getList({}, pageIndex, pageSize); + }); + + async deleteList(keys: IDType[]) { + const { translater, store } = this.props; + + if (confirm(translater.t('sure_to_delete_x', { keys }))) + for (const key of keys) await store.deleteOne(key); + } + render() { - const { className, editable, store, translater } = this.props; - const { indexKey, pageIndex, pageCount, totalCount } = store, + const { className, editable, deletable, store, translater } = this.props; + const { indexKey, pageSize, pageIndex, pageCount, totalCount } = store, { t } = translater; return ( @@ -213,8 +330,8 @@ export class RestTable extends PureComponent<
- {editable && ( - - )} +
+ {deletable && ( + + )} + {editable && ( + + )} +
{this.renderTable()} diff --git a/source/index.ts b/source/index.ts index 6074bef..a4d5284 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,2 +1,3 @@ export * from './Pager'; +export * from './FormField'; export * from './RestTable';