tachybase_todo/packages/client/src/schemas/table/index.tsx
2021-07-16 23:57:11 +08:00

635 lines
17 KiB
TypeScript

import React, { createContext, useContext, useRef, useState } from 'react';
import {
useFieldSchema,
Schema,
observer,
RecursionField,
useField,
useForm,
FormProvider,
createSchemaField,
SchemaOptionsContext,
} from '@formily/react';
import {
Button,
Pagination,
PaginationProps,
Space,
Spin,
Table as AntdTable,
Dropdown,
Menu,
} from 'antd';
import { findIndex, get } from 'lodash';
import constate from 'constate';
import useRequest from '@ahooksjs/use-request';
import { BaseResult } from '@ahooksjs/use-request/lib/types';
import { uid, clone } from '@formily/shared';
import { MenuOutlined } from '@ant-design/icons';
import {
SortableHandle,
SortableContainer,
SortableElement,
} from 'react-sortable-hoc';
import cls from 'classnames';
import { getSchemaPath, useDesignable, useSchemaPath, VisibleContext } from '../';
import './style.less';
interface TableRowProps {
index: number;
data: any;
}
const SortableRow = SortableElement((props: any) => <tr {...props} />);
const SortableBody = SortableContainer((props: any) => <tbody {...props} />);
const TableRowContext = createContext<TableRowProps>(null);
function usePaginationProps() {
const schema = useFieldSchema();
function findPagination(schema: Schema): Schema[] {
return schema.reduceProperties((columns, current) => {
if (current['x-component'] === 'Table.Pagination') {
return [...columns, current];
}
return [...columns, ...findPagination(current)];
}, []);
}
const pagination = findPagination(schema).shift();
if (pagination) {
// console.log({ pagination });
const props = pagination['x-component-props'] || {};
return { defaultCurrent: 1, defaultPageSize: 10, ...props };
}
return false;
}
function useDefaultAction() {
const schema = useFieldSchema();
function findDefaultAction(schema: Schema): Schema[] {
return schema.reduceProperties((columns, current) => {
if (current['x-default-action']) {
return [...columns, current];
}
return [...columns, ...findDefaultAction(current)];
}, []);
}
return findDefaultAction(schema).shift();
}
function useTableActionBars() {
const schema = useFieldSchema();
function findActionBars(schema: Schema) {
const actionBars = {
top: [],
bottom: [],
};
return schema.reduceProperties((bars, current) => {
if (current['x-component'] === 'Table.ActionBar') {
const align = current['x-component-props']?.['align'] || 'top';
bars[align].push(current);
return bars;
}
const nested = findActionBars(current);
Object.keys(nested).forEach((align) => {
bars[align].push(...nested[align]);
});
return bars;
}, actionBars);
}
return findActionBars(schema);
}
function useTableColumns(props?: any) {
const { schema } = useDesignable();
// const schema = useFieldSchema();
const { dataSource } = props || {};
function findColumns(schema: Schema): Schema[] {
return schema.reduceProperties((columns, current) => {
if (current['x-component'] === 'Table.Column') {
return [...columns, current];
}
return [...columns, ...findColumns(current)];
}, []);
}
return findColumns(schema).map((item) => {
const columnProps = item['x-component-props'] || {};
return {
title: <RecursionField name={item.name} schema={item} onlyRenderSelf />,
dataIndex: item.name,
...columnProps,
render(value, record, recordIndex) {
const index = dataSource.indexOf(record);
return (
<TableRowContext.Provider
value={{
index: index,
data: record,
}}
>
<RecursionField schema={item} name={index} onlyRenderProperties />
</TableRowContext.Provider>
);
},
};
});
}
export function useTableRow() {
const ctx = useContext(TableRowContext);
console.log('useTableRow', ctx.data);
return ctx.data;
}
export function useTableIndex() {
const {
params: { page, pageSize },
} = useTableContext();
const ctx = useContext(TableRowContext);
const field = useField();
if (!field.componentProps.isRemoteDataSource) {
return ctx.index;
}
return pageSize ? ctx.index + (page - 1) * pageSize : ctx.index;
}
export function useTableDestroyAction() {
const ctx = useContext(TableRowContext);
const { field, selectedRowKeys, setSelectedRowKeys, refresh } =
useTableContext();
const rowKey = field.componentProps.rowKey || 'id';
return {
async run() {
if (ctx && typeof ctx.index !== 'undefined') {
field.remove(ctx.index);
refresh();
return;
}
if (!selectedRowKeys.length) {
return;
}
while (selectedRowKeys.length) {
const key = selectedRowKeys.shift();
const index = findIndex(field.value, (item) => item[rowKey] === key);
field.remove(index);
}
refresh();
setSelectedRowKeys([]);
},
};
}
export function useTableFilterAction() {
const { field, refresh, params } = useTableContext();
const [,setVisible] = useContext(VisibleContext);
const form = useForm();
return {
async run() {
setVisible && setVisible(false);
refresh();
},
};
}
export function useTableCreateAction() {
const { field, run: exec, params } = useTableContext();
const [,setVisible] = useContext(VisibleContext);
const form = useForm();
return {
async run() {
setVisible && setVisible(false);
field.push({
key: uid(),
...clone(form.values),
});
form.reset();
exec({ ...params, page: 1 });
},
};
}
export function useTableUpdateAction() {
const { field, refresh, params } = useTableContext();
const [,setVisible] = useContext(VisibleContext);
const ctx = useContext(TableRowContext);
const form = useForm();
return {
async run() {
field.value[ctx.index] = form.values;
setVisible && setVisible(false);
refresh();
},
};
}
const [TableContextProvider, useTableContext] = constate(() => {
const field = useField<Formily.Core.Models.ArrayField>();
const schema = useFieldSchema();
const defaultSelectedRowKeys = useContext(SelectedRowKeysContext);
const [selectedRowKeys, setSelectedRowKeys] = useState(defaultSelectedRowKeys || []);
console.log({ defaultSelectedRowKeys })
const pagination = usePaginationProps();
const response = useRequest<{
list: any[];
total: number;
}>((params = {}) => {
return new Promise((resolve) => {
const dataSource = Array.isArray(field.value) ? field.value.slice() : [];
if (pagination) {
const {
page = pagination.defaultCurrent,
pageSize = pagination.defaultPageSize,
} = params;
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize - 1;
resolve({
list: dataSource.slice(startIndex, endIndex + 1),
total: dataSource.length,
});
} else {
resolve({
list: dataSource,
total: dataSource.length,
});
}
});
});
const params = {
page: pagination.defaultCurrent,
pageSize: pagination.defaultPageSize,
...(response.params[0] || {}),
};
return {
...response,
params,
field,
schema,
selectedRowKeys,
setSelectedRowKeys,
};
});
export { TableContextProvider, useTableContext };
export const SelectedRowKeysContext = createContext([]);
const TableContainer = observer((props) => {
const field = useField<Formily.Core.Models.ArrayField>();
const schema = useFieldSchema();
const actionBars = useTableActionBars();
const { loading, data, refresh, selectedRowKeys, setSelectedRowKeys } =
useTableContext();
const rowKey = field.componentProps.rowKey || 'id';
const defaultAction = useDefaultAction();
const dataSource = Array.isArray(field.value) ? field.value.slice() : [];
const columns = useTableColumns({ dataSource });
const ref = useRef<HTMLDivElement>();
const addTdStyles = (node: HTMLElement) => {
const helper = document.body.querySelector(`.nb-table-sort-helper`);
if (helper) {
const tds = node.querySelectorAll('td');
requestAnimationFrame(() => {
helper.querySelectorAll('td').forEach((td, index) => {
if (tds[index]) {
td.style.width = getComputedStyle(tds[index]).width;
}
});
});
}
};
return (
<div ref={ref} className={'nb-table'}>
{actionBars.top.map((actionBarSchema) => {
return (
<RecursionField
// onlyRenderProperties
schema={
new Schema({
type: 'object',
properties: {
[actionBarSchema.name]: actionBarSchema,
},
})
}
/>
);
})}
<AntdTable
pagination={false}
rowKey={rowKey}
loading={loading}
rowSelection={{
type: 'checkbox',
selectedRowKeys,
onChange: (keys) => {
console.log(keys);
setSelectedRowKeys(keys);
},
}}
dataSource={data?.list}
columns={columns}
components={{
body: {
wrapper: (props: any) => (
<SortableBody
useDragHandle
lockAxis="y"
// disableAutoscroll
helperClass={`nb-table-sort-helper`}
helperContainer={() => {
return ref.current?.querySelector('tbody');
}}
onSortStart={({ node }) => {
addTdStyles(node);
}}
onSortEnd={({ oldIndex, newIndex }) => {
field.move(oldIndex, newIndex);
refresh();
}}
{...props}
/>
),
row: (props: any) => {
const index = findIndex(
field.value,
(item) => item[rowKey] === props['data-row-key'],
);
return <SortableRow index={index} {...props} />;
},
},
}}
onRow={(data) => {
const index = dataSource.indexOf(data);
return {
onClick(e) {
if (!defaultAction) {
return;
}
const el = (e.target as HTMLElement);
if (
!el.classList.contains('ant-table-cell')
) {
return;
}
const btn = el.parentElement.querySelector<HTMLElement>(`.name-${defaultAction.name}`);
btn && btn.click();
},
};
}}
/>
{actionBars.bottom.map((actionBarSchema) => {
return (
<RecursionField
onlyRenderProperties
schema={
new Schema({
type: 'object',
properties: {
[actionBarSchema.name]: actionBarSchema,
},
})
}
/>
);
})}
</div>
);
});
export const Table: any = observer((props) => {
return (
<TableContextProvider>
<TableContainer />
</TableContextProvider>
);
});
Table.useTableRow = useTableRow;
Table.useTableIndex = useTableIndex;
Table.useTableDestroyAction = useTableDestroyAction;
Table.useTableFilterAction = useTableFilterAction;
Table.useTableCreateAction = useTableCreateAction;
Table.useTableUpdateAction = useTableUpdateAction;
function Blank() {
return null;
}
function useDesignableBar() {
const schema = useFieldSchema();
const options = useContext(SchemaOptionsContext);
const DesignableBar = get(options.components, schema['x-designable-bar']);
return {
DesignableBar: DesignableBar || Blank,
};
}
Table.Column = observer((props) => {
const schema = useFieldSchema();
const field = useField();
console.log('Table.Column', schema, field.title);
const { DesignableBar } = useDesignableBar();
return (
<div className={'nb-table-column'}>
{field.title}
<DesignableBar />
</div>
);
});
Table.Column.DesignableBar = () => {
const field = useField();
// const fieldSchema = useFieldSchema();
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
console.log('Table.Column.DesignableBar', { schema });
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item onClick={(e) => {
const title = uid();
field.title = title;
schema.title = title;
setVisible(false);
}}></Menu.Item>
<Menu.Item onClick={() => {
remove();
console.log('Table.Column.DesignableBar', { schema });
}}></Menu.Item>
<Menu.Item onClick={() => {
const name = uid();
insertAfter({
name: `column_${name}`,
type: 'void',
title: `字段 ${name}`,
'x-component': 'Table.Column',
'x-component-props': {
// title: 'z1',
},
'x-designable-bar': 'Table.Column.DesignableBar',
properties: {
[name]: {
type: 'string',
required: true,
// 'x-read-pretty': true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
})
}}></Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</span>
</div>
);
};
Table.ActionBar = observer((props) => {
return (
<div className={'action-bar'}>
<Space>{props.children}</Space>
</div>
);
});
Table.Pagination = observer((props) => {
const { data, params, run } = useTableContext();
return (
data?.total > params?.pageSize && (
<Pagination
{...props}
defaultCurrent={1}
defaultPageSize={5}
current={params?.page}
pageSize={params?.pageSize}
total={data?.total}
onChange={(page, pageSize) => {
run({ page, pageSize });
}}
/>
)
);
});
const SortHandle = SortableHandle((props: any) => {
return (
<MenuOutlined
{...props}
className={cls(`nb-table-sort-handle`, props.className)}
style={{ ...props.style }}
/>
);
}) as any;
Table.SortHandle = observer((props) => {
const field = useField<Formily.Core.Models.Field>();
console.log('SortHandle', field.value);
return <SortHandle {...props} />;
});
Table.Index = observer((props) => {
const index = useTableIndex();
const schema = useFieldSchema();
const field = useField<Formily.Core.Models.Field>();
const path = useSchemaPath();
return <div>#{index + 1}</div>;
});
Table.Addition = observer((props: any) => {
const { field, refresh } = useTableContext();
const current = useField();
return (
<Button
block
onClick={() => {
if (props.method === 'unshift') {
field.unshift({
key: uid(),
});
} else {
field.push({
key: uid(),
});
}
refresh();
}}
>
{current.title}
</Button>
);
});
Table.Action = () => null;
Table.Action.DesignableBar = () => {
const field = useField();
const path = useSchemaPath();
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
console.log('Table.Action.DesignableBar', path, field.address.entire, { schema, field });
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item onClick={(e) => {
schema.title = uid();
refresh();
}}></Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</span>
</div>
);
};