Skip to content

Commit 68311d5

Browse files
committed
[refactor] simplify Admin tables & form with REST Table component
1 parent bcffd59 commit 68311d5

File tree

4 files changed

+102
-323
lines changed

4 files changed

+102
-323
lines changed

components/PlatformAdmin/PlatformAdminModal.tsx

Lines changed: 0 additions & 70 deletions
This file was deleted.
Lines changed: 29 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,32 @@
1-
import 'array-unique-proposal';
1+
import { PlatformAdmin } from '@kaiyuanshe/openhackathon-service';
2+
import { Column, RestForm, SearchableInput } from 'mobx-restful-table';
23

3-
import { PlatformAdmin, Staff } from '@kaiyuanshe/openhackathon-service';
4-
import { observer } from 'mobx-react';
5-
import { FC, useContext } from 'react';
6-
import { Form, Table } from 'react-bootstrap';
4+
import { i18n } from '../../models/Base/Translation';
5+
import userStore from '../../models/User';
76

8-
import { i18n, I18nContext } from '../../models/Base/Translation';
9-
import styles from '../../styles/Table.module.less';
10-
import { convertDatetime } from '../../utils/time';
11-
import { XScrollListProps } from '../layout/ScrollList';
12-
13-
const TableHeads = ({ t }: typeof i18n) => [
14-
t('all'),
15-
t('name'),
16-
t('mail'),
17-
t('phone_number'),
18-
t('role_type'),
19-
t('status'),
20-
// t('last_login_time'),
21-
t('create_time'),
22-
t('remark'),
23-
];
24-
25-
export const HackathonAdminList: FC<XScrollListProps<Staff | PlatformAdmin>> = observer(
26-
({ defaultData = [], selectedIds = [], onSelect }) => {
27-
const i18n = useContext(I18nContext);
28-
const { t } = i18n;
29-
30-
return (
31-
<Table hover responsive="lg" className={styles.table}>
32-
<thead>
33-
<tr>
34-
{TableHeads(i18n).map((data, idx) =>
35-
idx ? (
36-
<th key={data}>{data}</th>
37-
) : (
38-
<th key={data}>
39-
<Form.Check
40-
// https://github.com/facebook/react/issues/1798
41-
ref={(input: HTMLInputElement | null) => {
42-
if (input)
43-
input.indeterminate =
44-
!!selectedIds.length && selectedIds.length < defaultData.length;
45-
}}
46-
inline
47-
type="checkbox"
48-
name="selectAll"
49-
aria-label="selectAll"
50-
checked={selectedIds.length === defaultData.length}
51-
onChange={() =>
52-
onSelect?.(
53-
selectedIds.length === defaultData.length
54-
? []
55-
: defaultData.map(({ user: { id } }) => id),
56-
)
57-
}
58-
/>
59-
</th>
60-
),
61-
)}
62-
</tr>
63-
</thead>
64-
<tbody>
65-
{defaultData.map(({ createdAt, user: { id, email, mobilePhone, name }, description }) => (
66-
<tr key={id}>
67-
{[
68-
id,
69-
name,
70-
email,
71-
mobilePhone,
72-
description ? t('referee') : t('admin'),
73-
createdAt ? t('approve') : t('status_pending'),
74-
// convertDatetime(lastLogin),
75-
convertDatetime(createdAt),
76-
description,
77-
].map((data, idx) =>
78-
idx ? (
79-
<td key={id + createdAt}>{data}</td>
80-
) : (
81-
<td key={id + createdAt}>
82-
<Form.Check
83-
inline
84-
type="checkbox"
85-
name="userId"
86-
value={data}
87-
aria-label={description ? `judge${data}` : `admin${data}`}
88-
checked={selectedIds.includes(id)}
89-
onChange={
90-
onSelect &&
91-
(({ currentTarget: { checked } }) => {
92-
if (checked) return onSelect([...selectedIds, id].uniqueBy());
93-
94-
const index = selectedIds.indexOf(id);
95-
96-
onSelect([
97-
...selectedIds.slice(0, index),
98-
...selectedIds.slice(index + 1),
99-
]);
100-
})
101-
}
102-
/>
103-
</td>
104-
),
105-
)}
106-
</tr>
107-
))}
108-
</tbody>
109-
</Table>
110-
);
7+
export const adminColumns = <T extends PlatformAdmin>(translator: typeof i18n): Column<T>[] => [
8+
{
9+
key: 'user',
10+
renderHead: translator.t('user'),
11+
renderBody: ({ user: { name, email, mobilePhone } }) => (
12+
<div className="d-flex align-items-center gap-1">
13+
{name}
14+
<a href={`mailto:${email}`}>📧</a>
15+
<a href={`tel:${mobilePhone}`}>📱</a>
16+
</div>
17+
),
18+
renderInput: ({ user }, { key, ...meta }) => (
19+
<RestForm.FieldBox name={key} {...meta}>
20+
<SearchableInput
21+
translator={translator}
22+
store={userStore}
23+
labelKey="email"
24+
valueKey="id"
25+
name={key as string}
26+
defaultValue={[{ value: user?.id.toString(), label: user?.email || '' }]}
27+
/>
28+
</RestForm.FieldBox>
29+
),
11130
},
112-
);
31+
{ key: 'description', renderHead: translator.t('description') },
32+
];
Lines changed: 58 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import { faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
2-
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3-
import { observable } from 'mobx';
1+
import { Staff, StaffType } from '@kaiyuanshe/openhackathon-service';
2+
import { Loading } from 'idea-react';
3+
import { computed } from 'mobx';
44
import { observer } from 'mobx-react';
55
import { ObservedComponent } from 'mobx-react-helper';
6-
import { ScrollList } from 'mobx-restful-table';
6+
import { Column, RestTable } from 'mobx-restful-table';
77
import { compose, RouteProps, router } from 'next-ssr-middleware';
8-
import { FC, FormEvent, useContext } from 'react';
9-
import { Badge, Button, Col, Form, ListGroup, Row } from 'react-bootstrap';
8+
import { FC, useContext } from 'react';
9+
import { Badge, Col, ListGroup, Row } from 'react-bootstrap';
1010

1111
import { ActivityManageFrame } from '../../../../components/Activity/ActivityManageFrame';
12-
import { AdministratorModal } from '../../../../components/User/ActivityAdministratorModal';
13-
import { HackathonAdminList } from '../../../../components/User/HackathonAdminList';
12+
import { adminColumns } from '../../../../components/User/HackathonAdminList';
1413
import activityStore from '../../../../models/Activity';
1514
import { i18n, I18nContext } from '../../../../models/Base/Translation';
1615
import { sessionGuard } from '../../../api/core';
@@ -40,46 +39,56 @@ class AdministratorEditor extends ObservedComponent<AdministratorPageProps, type
4039

4140
store = activityStore.staffOf(this.props.route.params!.name + '');
4241

43-
@observable
44-
accessor selectedIds: number[] = [];
45-
46-
@observable
47-
accessor show = false;
48-
49-
//处理删除管理员或裁判
50-
handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
51-
event.preventDefault();
52-
event.stopPropagation();
53-
54-
const { t } = this.observedContext,
55-
{ selectedIds } = this;
56-
57-
if (!selectedIds[0]) return alert(t('please_select_at_least_one_user'));
58-
59-
if (!confirm(t('confirm_to_delete_admin_or_referee'))) return;
60-
61-
for (const id of selectedIds) await this.store.deleteOne(id);
62-
};
42+
@computed
43+
get columns() {
44+
const i18n = this.observedContext;
45+
const { t } = i18n;
46+
47+
return [
48+
{
49+
key: 'type',
50+
renderHead: t('type'),
51+
type: 'radio',
52+
options: [
53+
{ value: 'admin', label: t('admin') },
54+
{ value: 'judge', label: t('referee') },
55+
],
56+
},
57+
...adminColumns(i18n),
58+
] as Column<Staff>[];
59+
}
6360

6461
renderList() {
6562
const { t } = this.observedContext,
6663
{ allItems, typeCount } = this.store;
6764

6865
return (
6966
<ListGroup>
70-
<ListGroup.Item className="d-flex justify-content-between">
67+
<ListGroup.Item
68+
className="d-flex justify-content-between"
69+
action
70+
onClick={() => this.store.getList({}, 1)}
71+
>
7172
{t('all_user')}
7273
<Badge className="ms-2" bg="secondary">
7374
{allItems.length}
7475
</Badge>
7576
</ListGroup.Item>
76-
<ListGroup.Item className="d-flex justify-content-between">
77+
<ListGroup.Item
78+
className="d-flex justify-content-between"
79+
action
80+
onClick={() => this.store.getList({ type: 'admin' as StaffType.Admin }, 1)}
81+
>
7782
{t('admin')}
7883
<Badge className="ms-2" bg="secondary">
7984
{typeCount.admin}
8085
</Badge>
8186
</ListGroup.Item>
82-
<ListGroup.Item className="d-flex justify-content-between">
87+
<ListGroup.Item
88+
className="d-flex justify-content-between"
89+
action
90+
onClick={() => this.store.getList({ type: 'judge' as StaffType.Judge }, 1)}
91+
>
8392
{t('referee')}
8493
<Badge className="ms-2" bg="secondary">
8594
{typeCount.judge}
@@ -91,57 +100,26 @@ class AdministratorEditor extends ObservedComponent<AdministratorPageProps, type
91100

92101
render() {
93102
const i18n = this.observedContext,
94-
{ store, show } = this;
95-
const { t } = i18n,
96-
loading = store.uploading > 0;
103+
{ downloading, uploading } = this.store;
97104

98-
return (
99-
<Form onSubmit={this.handleSubmit}>
100-
<Row xs="1" sm="2">
101-
<Col sm="auto" md="auto">
102-
{this.renderList()}
105+
const loading = downloading > 0 || uploading > 0;
103106

104-
<Col className="d-flex flex-column">
105-
<Button
106-
variant="success"
107-
className="my-3"
108-
disabled={loading}
109-
onClick={() => (this.show = true)}
110-
>
111-
<FontAwesomeIcon className="me-2" icon={faPlus} />
112-
{t('add')}
113-
</Button>
114-
<Button variant="danger" type="submit" disabled={loading}>
115-
<FontAwesomeIcon className="me-2" icon={faTrash} />
116-
{t('delete')}
117-
</Button>
118-
</Col>
119-
</Col>
120-
<Col className="flex-fill">
121-
<ScrollList
122-
translator={i18n}
123-
store={store}
124-
renderList={allItems => (
125-
<HackathonAdminList
126-
defaultData={allItems}
127-
selectedIds={this.selectedIds}
128-
onSelect={list => (this.selectedIds = list)}
129-
/>
130-
)}
131-
/>
132-
</Col>
133-
</Row>
134-
135-
<AdministratorModal
136-
store={store}
137-
show={show}
138-
onHide={() => (this.show = false)}
139-
onSave={() => {
140-
this.show = false;
141-
this.store.refreshList();
142-
}}
143-
/>
144-
</Form>
107+
return (
108+
<Row xs="1" sm="2">
109+
<Col sm="auto" md="auto">
110+
{this.renderList()}
111+
</Col>
112+
<Col className="flex-fill">
113+
<RestTable
114+
translator={i18n}
115+
store={this.store}
116+
columns={this.columns}
117+
editable
118+
deletable
119+
/>
120+
{loading && <Loading />}
121+
</Col>
122+
</Row>
145123
);
146124
}
147125
}

0 commit comments

Comments
 (0)