Skip to content

Commit e24ff76

Browse files
authored
[add] Filter Bar on the top of REST Table (#25)
1 parent 382670d commit e24ff76

7 files changed

Lines changed: 805 additions & 888 deletions

File tree

package.json

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mobx-restful-table",
3-
"version": "2.5.5",
3+
"version": "2.6.0",
44
"license": "LGPL-3.0",
55
"author": "shiy2008@gmail.com",
66
"description": "A Pagination Table & Scroll List component suite for CRUD operation, which is based on MobX RESTful & React.",
@@ -36,32 +36,32 @@
3636
"react-bootstrap": "^2.10.10",
3737
"react-bootstrap-editor": "^2.1.1",
3838
"regenerator-runtime": "^0.14.1",
39-
"web-utility": "^4.6.2"
39+
"web-utility": "^4.6.4"
4040
},
4141
"peerDependencies": {
4242
"react": ">=16.8"
4343
},
4444
"devDependencies": {
45-
"@octokit/openapi-types": "^26.0.0",
46-
"@parcel/config-default": "~2.16.0",
47-
"@parcel/packager-ts": "~2.16.0",
48-
"@parcel/transformer-typescript-tsc": "~2.16.0",
49-
"@parcel/transformer-typescript-types": "~2.16.0",
50-
"@types/lodash": "^4.17.20",
51-
"@types/node": "^22.18.11",
52-
"@types/react": "^19.2.2",
53-
"@types/react-dom": "^19.2.2",
45+
"@octokit/openapi-types": "^27.0.0",
46+
"@parcel/config-default": "~2.16.1",
47+
"@parcel/packager-ts": "~2.16.1",
48+
"@parcel/transformer-typescript-tsc": "~2.16.1",
49+
"@parcel/transformer-typescript-types": "~2.16.1",
50+
"@types/lodash": "^4.17.21",
51+
"@types/node": "^22.19.1",
52+
"@types/react": "^19.2.6",
53+
"@types/react-dom": "^19.2.3",
5454
"husky": "^9.1.7",
5555
"idea-react": "^2.0.0-rc.13",
5656
"koajax": "^3.1.2",
57-
"lint-staged": "^16.2.5",
58-
"mobx-github": "^0.6.0",
59-
"parcel": "~2.16.0",
57+
"lint-staged": "^16.2.7",
58+
"mobx-github": "^0.6.2",
59+
"parcel": "~2.16.1",
6060
"prettier": "^3.6.2",
6161
"prismjs": "^1.30.0",
6262
"react": "^19.2.0",
6363
"react-dom": "^19.2.0",
64-
"rimraf": "^6.0.1",
64+
"rimraf": "^6.1.2",
6565
"typedoc": "^0.28.14",
6666
"typedoc-plugin-mdn-links": "^5.0.10",
6767
"typescript": "~5.9.3"

pnpm-lock.yaml

Lines changed: 685 additions & 838 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

preview/content.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GitRepository } from 'mobx-github';
1+
import { GitRepository, RepositoryFilter } from 'mobx-github';
22
import { FC } from 'react';
33
import { Form, InputGroup } from 'react-bootstrap';
44

@@ -7,6 +7,7 @@ import {
77
BadgeBar,
88
BadgeInput,
99
Column,
10+
Field,
1011
FileModel,
1112
FilePicker,
1213
FilePreview,
@@ -30,6 +31,14 @@ class MyFileModel extends FileModel {}
3031

3132
const fileStore = new MyFileModel();
3233

34+
const filterFields: Field<RepositoryFilter>[] = [
35+
{ key: 'full_name', renderLabel: 'Repository Name' },
36+
{ key: 'homepage', renderLabel: 'Home Page' },
37+
{ key: 'language', renderLabel: 'Programming Language' },
38+
{ key: 'topics', renderLabel: 'Topic' },
39+
{ key: 'description', renderLabel: 'Description' },
40+
];
41+
3342
const columns: Column<GitRepository>[] = [
3443
{
3544
key: 'full_name',
@@ -211,6 +220,7 @@ export const Content: FC = () => (
211220
hover
212221
editable
213222
deletable
223+
filterFields={filterFields}
214224
columns={columns}
215225
store={repositoryStore}
216226
translator={i18n}

source/FileUploader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class FileUploader extends FormComponent<FileUploaderProps> {
7474
super.componentDidMount();
7575

7676
const { store } = this.props;
77-
debugger
77+
7878
store.files = this.value || [];
7979
}
8080

source/RestForm.tsx

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
44
import { ObservedComponent } from 'mobx-react-helper';
55
import { DataObject, Filter, IDType, ListModel } from 'mobx-restful';
66
import { FormEvent, Fragment, InputHTMLAttributes, ReactNode } from 'react';
7-
import { Button, Form, FormGroupProps, FormProps, InputGroup } from 'react-bootstrap';
7+
import { Button, ButtonProps, Form, FormGroupProps, FormProps, InputGroup } from 'react-bootstrap';
88
import { Editor, EditorProps } from 'react-bootstrap-editor';
99
import { formatDate, formToJSON, isEmpty } from 'web-utility';
1010

@@ -46,12 +46,14 @@ export interface FieldBoxProps<D extends DataObject>
4646
}
4747

4848
export interface RestFormProps<D extends DataObject, F extends Filter<D> = Filter<D>>
49-
extends Pick<FormProps, 'className' | 'style'> {
49+
extends Pick<FormProps, 'className' | 'style'>,
50+
Pick<ButtonProps, 'size'> {
5051
id?: IDType;
5152
fields: Field<D>[];
52-
store: ListModel<D, F>;
53+
store?: ListModel<D, F>;
5354
translator: TranslationModel<string, 'submit' | 'cancel'>;
5455
onSubmit?: (data: D) => any;
56+
onReset?: (data: Partial<D>) => any;
5557
}
5658

5759
@observer
@@ -104,7 +106,7 @@ export class RestForm<
104106
componentDidMount() {
105107
const { id, store } = this.props;
106108

107-
if (id) store.getOne(id);
109+
if (id) store?.getOne(id);
108110
}
109111

110112
handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
@@ -118,16 +120,24 @@ export class RestForm<
118120

119121
if (valid) {
120122
const { id, store, onSubmit } = this.props;
123+
let data = formToJSON<D>(form);
121124

122-
const updated = await store.updateOne(formToJSON(form), id);
125+
data = await store?.updateOne(data, id);
123126

124-
onSubmit?.(updated);
127+
onSubmit?.(data);
125128

126-
store.clearCurrent();
129+
store?.clearCurrent();
127130
}
128131
this.validated = false;
129132
};
130133

134+
handleReset = ({ currentTarget }: FormEvent<HTMLFormElement>) => {
135+
const { onReset, store } = this.props;
136+
137+
onReset?.(formToJSON(currentTarget));
138+
store?.clearCurrent();
139+
};
140+
131141
@computed
132142
get fields(): Field<D>[] {
133143
const { fields } = this.observedProps;
@@ -163,7 +173,7 @@ export class RestForm<
163173
get fieldReady() {
164174
const { id, store } = this.observedProps;
165175

166-
return !id || store.downloading < 1;
176+
return !id || store?.downloading < 1;
167177
}
168178

169179
renderFile =
@@ -239,6 +249,7 @@ export class RestForm<
239249
{...props}
240250
{...meta}
241251
{...{ type, step, label }}
252+
size={this.observedProps.size}
242253
name={key.toString()}
243254
defaultValue={RestForm.dateValueOf({ type, step }, value)}
244255
/>
@@ -259,29 +270,41 @@ export class RestForm<
259270

260271
render() {
261272
const { fields, readOnly, customValidation, validated } = this,
262-
{ id, className = '', store, translator, ...props } = this.observedProps;
263-
const { downloading, uploading, currentOne } = store,
273+
{
274+
id,
275+
className = 'd-flex flex-column gap-3',
276+
size,
277+
store,
278+
translator,
279+
...props
280+
} = this.props;
281+
const { downloading, uploading, currentOne = {} as D } = store || {},
264282
{ t } = translator;
265283
const loading = downloading > 0 || uploading > 0;
266284

267285
return (
268286
<Form
269-
className={`d-flex flex-column gap-3 ${className}`}
270287
{...props}
288+
{...{ className, validated }}
271289
noValidate={customValidation}
272-
validated={validated}
273290
onSubmit={this.handleSubmit}
274-
onReset={() => store.clearCurrent()}
291+
onReset={this.handleReset}
275292
>
276293
{fields.map(({ renderInput, ...meta }) => (
277294
<Fragment key={meta.key?.toString()}>{renderInput?.(currentOne, meta)}</Fragment>
278295
))}
279296
{!readOnly && (
280297
<footer className="d-flex gap-3">
281-
<Button className="flex-fill" type="submit" disabled={loading}>
298+
<Button className="flex-fill" size={size} type="submit" disabled={loading}>
282299
{t('submit')}
283300
</Button>
284-
<Button className="flex-fill" type="reset" variant="danger" disabled={loading}>
301+
<Button
302+
className="flex-fill"
303+
size={size}
304+
type="reset"
305+
variant="danger"
306+
disabled={loading}
307+
>
285308
{t('cancel')}
286309
</Button>
287310
</footer>

source/RestFormModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { isEmpty } from 'web-utility';
66
import { RestForm, RestFormProps } from './RestForm';
77

88
export const RestFormModal = observer(
9-
<T extends DataObject>({ fields, store, translator }: RestFormProps<T>) => {
9+
<T extends DataObject>({ fields, store, translator, ...props }: RestFormProps<T>) => {
1010
const { indexKey, currentOne } = store;
1111

1212
const editing = !isEmpty(currentOne),
@@ -17,7 +17,7 @@ export const RestFormModal = observer(
1717
<Modal.Header closeButton>{ID}</Modal.Header>
1818

1919
<Modal.Body>
20-
<RestForm id={ID} {...{ fields, store, translator }} />
20+
<RestForm id={ID} {...{ fields, store, translator, ...props }} />
2121
</Modal.Body>
2222
</Modal>
2323
);

source/RestTable.tsx

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { isEmpty } from 'web-utility';
1212
import { BadgeBar } from './BadgeBar';
1313
import { FilePreview } from './FilePreview';
1414
import { Pager } from './Pager';
15-
import { Field, RestFormProps } from './RestForm';
15+
import { Field, RestForm, RestFormProps } from './RestForm';
1616
import { RestFormModal } from './RestFormModal';
1717

1818
export interface Column<T extends DataObject> extends Omit<Field<T>, 'renderLabel'> {
@@ -27,9 +27,10 @@ type Translator<T extends DataObject> = RestFormProps<T>['translator'] &
2727
'create' | 'view' | 'edit' | 'delete' | 'total_x_rows' | 'sure_to_delete_x'
2828
>;
2929
export interface RestTableProps<D extends DataObject, F extends Filter<D> = Filter<D>>
30-
extends Omit<TableProps, 'onSubmit'>,
31-
Omit<RestFormProps<D>, 'id' | 'fields' | 'translator'> {
30+
extends Omit<TableProps, 'onSubmit' | 'onReset'>,
31+
Omit<RestFormProps<D>, 'id' | 'size' | 'fields' | 'translator'> {
3232
filter?: F;
33+
filterFields?: Field<F>[];
3334
editable?: boolean;
3435
deletable?: boolean;
3536
columns: Column<D>[];
@@ -51,6 +52,13 @@ export class RestTable<
5152
store.getList(filter);
5253
}
5354

55+
@computed
56+
get fieldSize() {
57+
const { size } = this.observedProps;
58+
59+
return !size ? undefined : size === 'sm' ? 'sm' : 'lg';
60+
}
61+
5462
@observable
5563
accessor checkedKeys: IDType[] = [];
5664

@@ -113,7 +121,8 @@ export class RestTable<
113121

114122
@computed
115123
get operateColumn(): Column<D> {
116-
const { editable, deletable, columns, store, translator } = this.observedProps;
124+
const { editable, deletable, columns, store, translator } = this.observedProps,
125+
{ fieldSize } = this;
117126
const { t } = translator,
118127
readOnly = columns.every(({ readOnly }) => readOnly),
119128
disabled = columns.every(({ disabled }) => disabled);
@@ -129,7 +138,7 @@ export class RestTable<
129138
<Button
130139
className="text-nowrap m-1"
131140
variant={readOnly ? 'primary' : 'warning'}
132-
size="sm"
141+
size={fieldSize}
133142
onClick={() => (store.currentOne = data)}
134143
>
135144
{readOnly ? t('view') : t('edit')}
@@ -140,7 +149,7 @@ export class RestTable<
140149
<Button
141150
className="text-nowrap m-1"
142151
variant="danger"
143-
size="sm"
152+
size={fieldSize}
144153
onClick={() => this.deleteList([data.id])}
145154
>
146155
{t('delete')}
@@ -225,6 +234,7 @@ export class RestTable<
225234
deletable,
226235
onCheck,
227236
onSubmit,
237+
onReset,
228238
responsive = true,
229239
...tableProps
230240
} = this.props,
@@ -306,30 +316,50 @@ export class RestTable<
306316
}
307317

308318
render() {
309-
const { className, editable, deletable, store, translator, onSubmit } = this.props;
319+
const {
320+
className = 'overflow-auto d-flex flex-column gap-3',
321+
editable,
322+
deletable,
323+
filterFields,
324+
store,
325+
translator,
326+
onSubmit,
327+
onReset,
328+
...props
329+
} = this.props,
330+
{ fieldSize } = this;
310331
const { t } = translator,
311332
{ indexKey, pageSize, pageIndex, pageCount, totalCount } = store;
312333

313334
return (
314-
<div className={classNames('overflow-auto', className)}>
315-
<header className="d-flex justify-content-between sticky-top bg-white py-3">
316-
<nav className="d-flex align-items-center">
317-
<Pager {...{ pageSize, pageIndex, pageCount }} onChange={this.getList} />
318-
319-
{!!totalCount && <span className="mx-3 fs14">{t('total_x_rows', { totalCount })}</span>}
320-
</nav>
321-
<div>
335+
<div className={className} {...props}>
336+
<header className="sticky-top bg-white py-3">
337+
{filterFields && (
338+
<RestForm
339+
className="d-flex flex-wrap align-items-center gap-3 pb-3 border-bottom"
340+
size={fieldSize}
341+
translator={translator}
342+
fields={filterFields}
343+
onSubmit={filter => store.getList(filter, 1)}
344+
onReset={() => store.getList({}, 1)}
345+
/>
346+
)}
347+
<div className="d-flex justify-content-between align-items-center">
322348
{deletable && (
323349
<Button
324350
className="mx-2"
325351
variant="danger"
352+
size={fieldSize}
326353
onClick={() => this.deleteList(this.checkedKeys)}
327354
>
328355
{t('delete')}
329356
</Button>
330357
)}
331358
{editable && (
332-
<Button onClick={() => (store.currentOne[indexKey] = '' as D[keyof D])}>
359+
<Button
360+
size={fieldSize}
361+
onClick={() => (store.currentOne[indexKey] = '' as D[keyof D])}
362+
>
333363
{t('create')}
334364
</Button>
335365
)}
@@ -338,8 +368,15 @@ export class RestTable<
338368

339369
{this.renderTable()}
340370

371+
<footer className="d-flex justify-content-between align-items-center">
372+
{!!totalCount && <span className="mx-3 fs14">{t('total_x_rows', { totalCount })}</span>}
373+
374+
<Pager {...{ pageSize, pageIndex, pageCount }} onChange={this.getList} />
375+
</footer>
376+
341377
{editable && (
342378
<RestFormModal
379+
size={fieldSize}
343380
fields={this.columns.map(({ renderHead, ...field }) => ({
344381
...field,
345382
renderLabel: renderHead,

0 commit comments

Comments
 (0)