lots of updates

This commit is contained in:
chenos 2021-08-04 12:38:08 +08:00
parent c23d2c5c3c
commit 5b014bc67e
28 changed files with 951 additions and 194 deletions

View File

@ -21,6 +21,7 @@ import * as uiSchema from './ui-schema';
const Collection = database.getModel('collections');
const collection = await Collection.create(config);
await collection.updateAssociations(config);
await collection.migrate();
const Route = database.getModel('routes');

View File

@ -29,6 +29,7 @@
"axios": "^0.21.1",
"beautiful-react-hooks": "^0.35.0",
"constate": "^3.3.0",
"flat": "^5.0.2",
"html-react-parser": "^1.2.7",
"lodash": "^4.17.21",
"monaco-editor": "^0.25.2",

View File

@ -19,7 +19,7 @@ import { useMemo } from 'react';
import { CodeOutlined } from '@ant-design/icons';
import Editor from '@monaco-editor/react';
import { ArrayCollapse, FormLayout, FormItem as FormilyFormItem } from '@formily/antd';
import { ArrayItems, ArrayCollapse, FormLayout, FormItem as FormilyFormItem } from '@formily/antd';
import { Space, Card, Modal, Spin } from 'antd';
import { ArrayTable } from '../../schemas/array-table';
@ -74,6 +74,7 @@ export const SchemaField = createSchemaField({
Page,
Chart,
ArrayItems,
ArrayCollapse,
ArrayTable,
FormLayout,
@ -365,6 +366,47 @@ export function useDesignable(path?: any) {
refresh();
return target;
},
moveToBefore(path1, path2) {
const source = findPropertyByPath(schema, path1);
const property = source.toJSON();
const target = findPropertyByPath(schema, path2);
if (!target) {
console.error('target schema does not exist.');
return;
}
if (!property.name) {
property.name = uid();
}
if (target['parentKey']) {
property['parentKey'] = target['parentKey'];
setKeys(property);
property['__insertBefore__'] = target['key'];
}
addPropertyBefore(target, property);
refresh();
return target.parent.properties[property.name];
},
moveToAfter(path1, path2) {
const source = findPropertyByPath(schema, path1);
const property = source.toJSON();
const target = findPropertyByPath(schema, path2);
if (!target) {
console.error('target schema does not exist.');
return;
}
if (!property.name) {
property.name = uid();
}
if (target['parentKey']) {
property['parentKey'] = target['parentKey'];
setKeys(property);
property['__insertAfter__'] = target['key'];
}
addPropertyAfter(target, property);
refresh();
return target.parent.properties[property.name];
},
};
}

View File

@ -14,8 +14,7 @@ export interface CollectionProviderProps {
collectionName?: string;
}
const [CollectionProvider, useCollectionContext] = constate(
(props: CollectionProviderProps) => {
export function useCollection(props: CollectionProviderProps) {
const { collectionName } = props;
const { collections = [], loading, refresh } = useCollectionsContext();
let collection: any;
@ -26,7 +25,12 @@ const [CollectionProvider, useCollectionContext] = constate(
if (collection) {
fields = collection?.fields || [];
}
let sortableField = collection.sortable;
if (collection.sortable && typeof collection.sortable === 'object') {
sortableField = collection.sortable?.name;
}
return {
sortableField,
collection,
fields,
loading,
@ -38,7 +42,8 @@ const [CollectionProvider, useCollectionContext] = constate(
return fields.find((item) => item.name === name);
},
};
},
);
}
const [CollectionProvider, useCollectionContext] = constate(useCollection);
export { CollectionProvider, useCollectionContext };

View File

@ -17,6 +17,8 @@ export interface SaveOptions {
}
export interface ListOptions {
defaultFilter?: any;
filter?: any;
}
export class Resource {
@ -31,9 +33,28 @@ export class Resource {
}
}
list(options: ListOptions = {}) {
sort(options) {
const { resourceName } = this.options;
return request(`${resourceName}:list`);
const { resourceKey, target, field = 'sort' } = options;
return request(`${resourceName}:sort/${resourceKey}`, {
method: 'post',
data: {
target,
field,
},
});
}
list(options: ListOptions = {}) {
const { defaultFilter, filter, ...others } = options;
const { resourceName } = this.options;
return request(`${resourceName}:list`, {
method: 'get',
params: {
filter: decodeURIComponent(JSON.stringify({ and: [defaultFilter, filter].filter(Boolean) })),
...others,
},
});
}
get(options: GetOptions = {}) {

View File

@ -7,6 +7,7 @@ export const createdAt: ISchema = {
group: 'systemInfo',
order: 1,
title: '创建时间',
sortable: true,
default: {
dataType: 'date',
field: 'created_at',

View File

@ -7,6 +7,7 @@ export const datetime: ISchema = {
group: 'datetime',
order: 1,
title: '日期',
sortable: true,
default: {
dataType: 'date',
// name,

View File

@ -7,6 +7,7 @@ export const email: ISchema = {
group: 'basic',
order: 4,
title: '电子邮箱',
sortable: true,
default: {
dataType: 'string',
// name,

View File

@ -7,6 +7,7 @@ export const number: ISchema = {
group: 'basic',
order: 5,
title: '数字',
sortable: true,
default: {
dataType: 'float',
// name,

View File

@ -7,6 +7,7 @@ export const percent: ISchema = {
group: 'basic',
order: 6,
title: '百分比',
sortable: true,
default: {
dataType: 'float',
// name,

View File

@ -7,6 +7,7 @@ export const phone: ISchema = {
group: 'basic',
order: 3,
title: '手机号码',
sortable: true,
default: {
dataType: 'string',
// name,

View File

@ -7,6 +7,7 @@ export const string: ISchema = {
group: 'basic',
order: 1,
title: '单行文本',
sortable: true,
default: {
interface: 'string',
dataType: 'string',

View File

@ -7,6 +7,7 @@ export const time: ISchema = {
group: 'datetime',
order: 2,
title: '时间',
sortable: true,
default: {
dataType: 'time',
// name,

View File

@ -7,6 +7,7 @@ export const updatedAt: ISchema = {
group: 'systemInfo',
order: 2,
title: '最后更新时间',
sortable: true,
default: {
dataType: 'date',
field: 'updated_at',

View File

@ -1,7 +1,7 @@
import React, { useContext, useMemo, useState } from 'react';
import { SchemaField } from '..';
import { createForm, onFieldChange, onFieldReact } from '@formily/core';
import { FormProvider, FormConsumer, useFieldSchema, Schema, SchemaOptionsContext, ISchema } from '@formily/react';
import { createForm, onFieldChange, onFieldReact, onFormValuesChange } from '@formily/core';
import { FormProvider, FormConsumer, useFieldSchema, Schema, SchemaOptionsContext, ISchema, SchemaKey } from '@formily/react';
import { Field } from '@formily/core/esm/models/Field';
import { Form } from '@formily/core/esm/models/Form';
import {
@ -14,26 +14,73 @@ import {
} from '@formily/antd';
import { CloseCircleOutlined } from '@ant-design/icons';
import { get } from 'lodash';
import { isValid } from '@formily/shared';
function useFilterColumns(): Schema[] {
function useFilterColumns(): Map<SchemaKey, Schema> {
const schema = useFieldSchema();
const columns = schema.reduceProperties((columns, current) => {
if (current['x-component'] === 'Filter.Column') {
return [...columns, current];
const fieldName = Object.keys(current.properties).shift();
columns.set(fieldName, current);
return columns;
}
return [...columns];
}, []);
return columns;
}, new Map<SchemaKey, Schema>());
return columns;
}
export const FilterItem = (props) => {
const { initialValues = {}, onRemove } = props;
const { value, initialValues = {}, onRemove, onChange } = props;
const options = useContext(SchemaOptionsContext);
const { key } = initialValues;
const columns = useFilterColumns();
console.log('useFilterColumns', columns);
console.log('FilterItem', value)
const toValues = (value) => {
if (!value) {
return {};
}
if (Object.keys(value).length === 0) {
return {};
}
const fieldName = Object.keys(value).shift();
const nested = value[fieldName];
const column = columns.get(fieldName).toJSON();
if (!nested) {
return {
column,
}
}
if (Object.keys(nested).length === 0) {
return {
column,
}
}
const operationValue = Object.keys(nested).shift();
console.log('toValues', {operationValue});
const operations = column?.['x-component-props']?.['operations']||[];
const operation = operations.find(operation => operation.value === operationValue);
console.log('toValues', {operation});
if (!operation) {
return {
column,
}
}
if (operation.noValue) {
return {
column,
operation,
}
}
return {
column,
operation,
value: nested[operationValue],
}
}
const values = toValues(value);
// console.log('toValues', values, value);
const Remove = (props) => {
return (
@ -53,7 +100,7 @@ export const FilterItem = (props) => {
const form = useMemo(
() =>
createForm({
initialValues,
initialValues: values,
effects: (form) => {
onFieldChange('column', (field: Field, form: Form) => {
const column = (field.value || {}) as ISchema;
@ -76,12 +123,28 @@ export const FilterItem = (props) => {
f.visible = !operation.noValue;
});
});
onFormValuesChange((form) => {
const { column, operation, value } = form.values;
if (!operation?.value) {
return;
}
const fieldName = Object.keys(column.properties).shift();
if (operation?.noValue) {
onChange({[fieldName]: {
[operation.value]: true,
}});
} else if (isValid(value)) {
onChange({[fieldName]: {
[operation.value]: value,
}});
}
console.log('form.values', form.values);
})
},
}),
[],
);
return (
<FormProvider form={form}>
<FormLayout layout={'inline'}>
@ -89,7 +152,7 @@ export const FilterItem = (props) => {
<SchemaField.Void x-component="Space">
<SchemaField.String
name="column"
x-decorator="FormItem"
x-decorator="FormilyFormItem"
x-decorator-props={{
asterisk: true,
feedbackLayout: 'none',
@ -105,11 +168,11 @@ export const FilterItem = (props) => {
value: 'name',
}
}}
enum={columns.map((column) => column.toJSON())}
enum={[...columns.values()].map((column) => column.toJSON())}
/>
<SchemaField.String
name="operation"
x-decorator="FormItem"
x-decorator="FormilyFormItem"
x-decorator-props={{
asterisk: true,
feedbackLayout: 'none',
@ -124,7 +187,7 @@ export const FilterItem = (props) => {
/>
<SchemaField.String
name="value"
x-decorator="FormItem"
x-decorator="FormilyFormItem"
x-decorator-props={{
asterisk: true,
feedbackLayout: 'none',

View File

@ -38,6 +38,20 @@ const schema = {
name: 'filter',
type: 'object',
'x-component': 'Filter',
default: {
and: [
{
field1: {
eq: 'aa',
}
},
{
field1: {
eq: 'bbb',
}
}
],
},
properties: {
column1: {
type: 'void',
@ -68,7 +82,7 @@ const schema = {
],
},
properties: {
field1: {
field2: {
type: 'number',
'x-component': 'InputNumber',
},
@ -79,7 +93,7 @@ const schema = {
export default () => {
return (
<SchemaRenderer schema={schema} />
<SchemaRenderer debug={true} schema={schema} />
);
};
```

View File

@ -5,12 +5,39 @@ import { useDynamicList } from 'ahooks';
import { Select } from 'antd';
import { CloseCircleOutlined } from '@ant-design/icons';
import { FilterItem } from './FilterItem';
import cls from 'classnames';
import './style.less';
const toValue = (value) => {
if (!value) {
return {
logical: 'and',
list: [{}],
};
}
if (value.and) {
return {
logical: 'and',
list: value.and,
};
}
if (value.or) {
return {
logical: 'and',
list: value.or,
};
}
return {
logical: 'and',
list: [{}],
};
};
export function FilterGroup(props) {
const { onRemove } = props;
const { bordered = true, onRemove, onChange } = props;
const value = toValue(props.value);
return (
<div className={'nb-filter-group'}>
<div className={cls('nb-filter-group', { bordered })}>
{onRemove && (
<a className={'nb-filter-group-close'} onClick={() => onRemove()}>
<CloseCircleOutlined />
@ -20,8 +47,13 @@ export function FilterGroup(props) {
{' '}
<Select
style={{ width: 80 }}
onChange={(value) => {}}
defaultValue={'and'}
onChange={(logical) => {
onChange &&
onChange({
[logical]: value.list,
});
}}
defaultValue={value.logical}
>
<Select.Option value={'and'}></Select.Option>
<Select.Option value={'or'}></Select.Option>
@ -29,9 +61,12 @@ export function FilterGroup(props) {
</div>
<FilterList
initialValue={[{}]}
onChange={(list) => {
console.log({ list });
initialValue={value.list}
onChange={(list: any[]) => {
onChange &&
onChange({
[value.logical]: list.filter((item) => Object.keys(item).length),
});
}}
/>
</div>
@ -40,7 +75,9 @@ export function FilterGroup(props) {
export function FilterList(props) {
const { initialValue } = props;
const { list, push, remove } = useDynamicList<any>(initialValue || []);
const { list, push, remove, replace } = useDynamicList<any>(
initialValue || [],
);
useEffect(() => {
props.onChange && props.onChange(list);
}, [list]);
@ -48,18 +85,29 @@ export function FilterList(props) {
<div className={'nb-filter-list'}>
<div>
{list.map((item, index) => {
if (item.type === 'group') {
return <FilterGroup key={index} onRemove={() => remove(index)} />;
if (item.and || item.or) {
return (
<FilterGroup
key={index}
value={item}
onChange={(value: any) => replace(index, value)}
onRemove={() => remove(index)}
/>
);
}
return <FilterItem key={index} onRemove={() => remove(index)} />;
return (
<FilterItem
key={index}
value={item}
onChange={(value: any) => replace(index, value)}
onRemove={() => remove(index)}
/>
);
})}
</div>
<a
onClick={() => {
push({
type: 'item',
key: new Date().toTimeString(),
});
push({});
}}
>
@ -67,8 +115,7 @@ export function FilterList(props) {
<a
onClick={() => {
push({
type: 'group',
key: new Date().toTimeString(),
and: [{}],
});
}}
>
@ -80,9 +127,10 @@ export function FilterList(props) {
export const Filter = connect(
(props) => {
// console.log('Filter.props', { props });
return (
<div>
<FilterGroup />
<FilterGroup bordered={false} {...props} />
</div>
);
},

View File

@ -1,10 +1,18 @@
.nb-filter-group {
position: relative;
margin-bottom: 14px;
padding: 14px;
border: 1px dashed rgb(222, 222, 222);
min-width: 400px;
&-close {
position: absolute;
right: 10px;
}
&.bordered {
border: 1px dashed rgb(222, 222, 222);
padding: 14px;
}
}
.nb-filter-list {
.ant-form-inline {
margin-bottom: 8px;
}
}

View File

@ -123,7 +123,6 @@ export function SortableBodyRow(props: any) {
<SortableContext
strategy={horizontalListSortingStrategy}
items={React.Children.map(props.children, (child) => {
console.log(child.key, 'child.key');
return `td${child.key}`;
})}
>

View File

@ -17,7 +17,7 @@ import { uid } from '@formily/shared';
import useRequest from '@ahooksjs/use-request';
import { BaseResult } from '@ahooksjs/use-request/lib/types';
import cls from 'classnames';
import { MenuOutlined, DragOutlined } from '@ant-design/icons';
import { MenuOutlined, DragOutlined, FilterOutlined } from '@ant-design/icons';
import { DndContext, DragOverlay } from '@dnd-kit/core';
import {
SortableContext,
@ -42,6 +42,7 @@ import { Resource } from '../../resource';
import {
CollectionProvider,
DisplayedMapProvider,
useCollection,
useCollectionContext,
useDisplayedMapContext,
} from '../../constate';
@ -59,7 +60,10 @@ import {
SortableRowHandle,
} from './Sortable';
import { DragHandle, Droppable, SortableItem } from '../../components/Sortable';
import { VisibleContext } from '../../context';
import { isValid } from '@formily/shared';
import { FormButtonGroup, FormDialog, FormLayout, Submit } from '@formily/antd';
import flatten from 'flat';
import IconPicker from '../../components/icon-picker';
export interface ITableContext {
props: any;
@ -79,12 +83,12 @@ export interface ITableRowContext {
record: any;
}
const TableConetxt = createContext<ITableContext>(null);
const TableContext = createContext<ITableContext>({} as any);
const TableRowContext = createContext<ITableRowContext>(null);
const CollectionFieldContext = createContext(null);
const useTable = () => {
return useContext(TableConetxt);
return useContext(TableContext);
};
const useTableRow = () => {
@ -98,10 +102,15 @@ function useTableFilterAction() {
refresh,
props: { refreshRequestOnChange },
} = useTable();
const form = useForm();
return {
async run() {
console.log('useTableFilterAction', form.values);
if (refreshRequestOnChange) {
return service.refresh();
return service.run({
...service.params[0],
// filter,
});
}
},
};
@ -204,7 +213,7 @@ const useTableIndex = () => {
const { pagination, props } = useTable();
const ctx = useContext(TableRowContext);
if (pagination && !props.clientSidePagination) {
const { pageSize, page } = pagination;
const { pageSize, page = 1 } = pagination;
return ctx.index + (page - 1) * pageSize;
}
return ctx.index;
@ -433,11 +442,13 @@ const useDataSource = () => {
const TableMain = () => {
const {
resource,
selectedRowKeys,
setSelectedRowKeys,
service,
field,
props: { rowKey, dragSort, showIndex },
refresh,
} = useTable();
const columns = useTableColumns();
const dataSource = useDataSource();
@ -446,8 +457,29 @@ const TableMain = () => {
return (
<div className={'nb-table'}>
<DndContext
onDragEnd={(event) => {
console.log({ event });
onDragEnd={async (event) => {
const fromId = event.active?.id as any;
const toId = event.over?.id as any;
if (isValid(fromId) && isValid(toId)) {
const fromIndex = findIndex(
field.value,
(item) => item[rowKey] === fromId,
);
const toIndex = findIndex(
field.value,
(item) => item[rowKey] === toId,
);
console.log({ fromId, toId, fromIndex, toIndex });
field.move(fromIndex, toIndex);
refresh();
await resource.sort({
resourceKey: fromId,
target: {
[rowKey]: toId,
},
});
await service.refresh();
}
}}
>
{actionBars.map((actionBar) => (
@ -541,13 +573,35 @@ const TableMain = () => {
);
};
const usePagination = (paginationProps?: any) => {
return useState(() => {
if (!paginationProps) {
return false;
}
return { page: 1, pageSize: 10, ...paginationProps };
});
const usePagination = () => {
const field = useField<Formily.Core.Models.ArrayField>();
const paginationProps = field.componentProps.pagination;
let pagination = paginationProps;
// const [pagination, setPagination] = useState(() => {
// if (!paginationProps) {
// return false;
// }
// const { defaultPageSize = 10, ...others } = paginationProps;
// return { page: 1, pageSize: defaultPageSize, ...others };
// });
// useEffect(() => {
// if (!paginationProps) {
// return setPagination(false);
// }
// const { defaultPageSize = 10, ...others } = paginationProps;
// setPagination({ page: 1, pageSize: defaultPageSize, ...others });
// }, [paginationProps]);
return [
pagination,
(params) => {
const defaults = field.componentProps.pagination;
field.componentProps.pagination = { ...defaults, ...params };
},
];
};
const TableProvider = (props: any) => {
@ -559,10 +613,25 @@ const TableProvider = (props: any) => {
} = props;
const { schema } = useDesignable();
const field = useField<Formily.Core.Models.ArrayField>();
const [pagination, setPagination] = usePagination(props.pagination);
const [pagination, setPagination] = usePagination();
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
const [, refresh] = useState(uid());
const { resource } = useResource();
const { sortableField } = useCollectionContext();
const dragSort = props.dragSort;
const getDefaultParams = () => {
const defaultParams = { ...pagination };
if (dragSort) {
defaultParams['sort'] = [sortableField || 'sort'];
} else {
defaultParams['sort'] = (props.defaultSort || []).join(',');
}
if (props.defaultFilter) {
defaultParams['defaultFilter'] = props.defaultFilter;
}
console.log({ defaultParams });
return defaultParams;
};
const service = useRequest(
(params?: any) => {
if (!resource) {
@ -582,12 +651,20 @@ const TableProvider = (props: any) => {
onSuccess(data: any) {
field.setValue(data?.list || []);
},
defaultParams: [{ ...pagination }],
manual: true,
// defaultParams: [getDefaultParams()],
},
);
console.log('refresh', { pagination });
useEffect(() => {
service.run(getDefaultParams());
}, [
pagination.pageSize,
props.dragSort,
props.defaultSort,
props.defaultFilter,
]);
return (
<TableConetxt.Provider
<TableContext.Provider
value={{
resource,
refresh: () => {
@ -597,7 +674,7 @@ const TableProvider = (props: any) => {
: service?.data?.total;
const maxPage = Math.ceil(total / pageSize);
if (page > maxPage) {
setPagination((prev) => ({ ...prev, page: maxPage }));
setPagination({ page: maxPage });
} else {
refresh(uid());
}
@ -613,7 +690,7 @@ const TableProvider = (props: any) => {
}}
>
<TableMain />
</TableConetxt.Provider>
</TableContext.Provider>
);
};
@ -654,19 +731,18 @@ Table.Pagination = observer(() => {
total={total}
onChange={(current, pageSize) => {
const page = pagination.pageSize !== pageSize ? 1 : current;
setPagination((prev) => ({
...prev,
page,
pageSize,
}));
if (clientSidePagination) {
return;
}
service.run({
...service.params,
setPagination({
page,
pageSize,
});
// if (clientSidePagination) {
// return;
// }
// service.run({
// ...service.params[0],
// page,
// pageSize,
// });
}}
/>
</div>
@ -1031,12 +1107,8 @@ Table.ActionBar = observer((props: any) => {
);
});
Table.Filter = observer((props: any) => {
const { fieldNames = [] } = props;
const { schema, DesignableBar } = useDesignable();
const form = useMemo(() => createForm(), []);
const { fields = [] } = useCollectionContext();
const fields2properties = (fields: any[]) => {
const fieldsToFilterColumns = (fields: any[], options: any = {}) => {
const { fieldNames = [] } = options;
const properties = {};
fields.forEach((field, index) => {
if (fieldNames?.length && !fieldNames.includes(field.name)) {
@ -1056,6 +1128,7 @@ Table.Filter = observer((props: any) => {
properties: {
[field.name]: {
...field.uiSchema,
'x-decorator': 'FormilyFormItem',
title: null,
},
},
@ -1063,10 +1136,45 @@ Table.Filter = observer((props: any) => {
});
return properties;
};
const fieldsToSortColumns = (fields: any[]) => {
const dataSource = [];
fields.forEach((field) => {
const fieldOption = interfaces.get(field.interface);
if (!fieldOption.sortable) {
return;
}
dataSource.push({
value: field.name,
label: field?.uiSchema?.title,
});
});
return dataSource;
};
Table.Filter = observer((props: any) => {
const { service } = useTable();
const { fieldNames = [] } = props;
const { schema, DesignableBar } = useDesignable();
const form = useMemo(() => createForm(), []);
const { fields = [] } = useCollectionContext();
const [visible, setVisible] = useState(false);
const obj = flatten(form.values.filter || {});
console.log('flatten', obj, Object.values(obj));
const count = Object.values(obj).filter((i) =>
Array.isArray(i) ? i.length : i,
).length;
const icon = props.icon || 'FilterOutlined';
return (
<Popover
trigger={['click']}
placement={'bottomLeft'}
visible={visible}
onVisibleChange={setVisible}
content={
<div>
<FormProvider form={form}>
@ -1077,17 +1185,32 @@ Table.Filter = observer((props: any) => {
filter: {
type: 'object',
'x-component': 'Filter',
properties: fields2properties(fields),
properties: fieldsToFilterColumns(fields, { fieldNames }),
},
},
}}
/>
<FormButtonGroup align={'right'}>
<Submit
onSubmit={() => {
const { filter } = form.values;
console.log('Table.Filter', form.values);
setVisible(false);
return service.run({
...service.params[0],
filter,
});
}}
>
</Submit>
</FormButtonGroup>
</FormProvider>
</div>
}
>
<Button>
{schema.title}
<Button icon={<IconPicker type={icon} />}>
{count > 0 ? `${count} 个筛选项` : schema.title}
<DesignableBar />
</Button>
</Popover>
@ -1099,7 +1222,11 @@ Table.Filter.DesignableBar = () => {
const [visible, setVisible] = useState(false);
const displayed = useDisplayedMapContext();
const { fields } = useCollectionContext();
const field = useField();
let fieldNames = field.componentProps.fieldNames || [];
if (fieldNames.length === 0) {
fieldNames = fields.map((field) => field.name);
}
return (
<div className={cls('designable-bar', { active: visible })}>
<span
@ -1119,22 +1246,72 @@ Table.Filter.DesignableBar = () => {
<Menu>
<Menu.ItemGroup title={'筛选字段'}>
{fields
.filter((field) => {
const option = interfaces.get(field.interface);
.filter((collectionField) => {
const option = interfaces.get(collectionField.interface);
return option.operations?.length;
})
.map((field) => (
.map((collectionField) => (
<SwitchMenuItem
title={field?.uiSchema?.title}
checked={true}
onChange={async (checked) => {}}
title={collectionField?.uiSchema?.title}
checked={fieldNames.includes(collectionField.name)}
onChange={async (checked) => {
if (checked) {
fieldNames.push(collectionField.name);
} else {
const index = fieldNames.indexOf(
collectionField.name,
);
if (index > -1) {
fieldNames.splice(index, 1);
}
}
console.log({ fieldNames, field });
schema['x-component-props']['fieldNames'] = fieldNames;
field.componentProps.fieldNames = fieldNames;
updateSchema(schema);
}}
/>
))}
</Menu.ItemGroup>
<Menu.Divider />
<Menu.Item
onClick={(e) => {
schema.title = uid();
onClick={async (e) => {
const values = await FormDialog('修改名称和图标', () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
title: {
type: 'string',
title: '按钮名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
icon: {
type: 'string',
title: '按钮图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
}}
/>
</FormLayout>
);
}).open({
initialValues: {
title: schema['title'],
icon: schema['x-component-props']?.['icon'],
},
});
schema['title'] = values.title;
schema['x-component-props']['icon'] = values.icon;
field.componentProps.icon = values.icon;
field.title = values.title;
updateSchema(schema);
refresh();
}}
>
@ -1153,7 +1330,7 @@ Table.Filter.DesignableBar = () => {
setVisible(false);
}}
>
</Menu.Item>
</Menu>
}
@ -1262,6 +1439,7 @@ Table.Action.DesignableBar = () => {
const isPopup = Object.keys(schema.properties || {}).length > 0;
const inActionBar = schema.parent['x-component'] === 'Table.ActionBar';
const displayed = useDisplayedMapContext();
const field = useField();
return (
<div className={cls('designable-bar', { active: visible })}>
<span
@ -1280,8 +1458,43 @@ Table.Action.DesignableBar = () => {
overlay={
<Menu>
<Menu.Item
onClick={(e) => {
schema.title = uid();
onClick={async (e) => {
const values = await FormDialog('修改名称和图标', () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
title: {
type: 'string',
title: '按钮名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
icon: {
type: 'string',
title: '按钮图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
}}
/>
</FormLayout>
);
}).open({
initialValues: {
title: schema['title'],
icon: schema['x-component-props']?.['icon'],
},
});
schema['title'] = values.title;
schema['x-component-props']['icon'] = values.icon;
field.componentProps.icon = values.icon;
field.title = values.title;
updateSchema(schema);
refresh();
}}
>
@ -1293,11 +1506,19 @@ Table.Action.DesignableBar = () => {
<Select
bordered={false}
size={'small'}
defaultValue={'modal'}
defaultValue={'Action.Modal'}
onChange={(value) => {
const s = Object.values(schema.properties).shift();
s['x-component'] = value;
refresh();
updateSchema(s);
// const f = field.query(getSchemaPath(s)).take()
// console.log('fffffff', { schema, f });
}}
>
<Select.Option value={'modal'}></Select.Option>
<Select.Option value={'drawer'}></Select.Option>
<Select.Option value={'window'}></Select.Option>
<Select.Option value={'Action.Modal'}></Select.Option>
<Select.Option value={'Action.Drawer'}></Select.Option>
<Select.Option value={'Action.Window'}></Select.Option>
</Select>{' '}
</Menu.Item>
@ -1321,7 +1542,7 @@ Table.Action.DesignableBar = () => {
setVisible(false);
}}
>
</Menu.Item>
</Menu>
}
@ -1389,7 +1610,8 @@ Table.Column.DesignableBar = () => {
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
const displayed = useDisplayedMapContext();
const ctx = useContext(ColDraggableContext);
const collectionField = useContext(CollectionFieldContext);
console.log('displayed.map', displayed.map);
return (
<div className={cls('designable-bar', { active: visible })}>
<span
@ -1409,7 +1631,38 @@ Table.Column.DesignableBar = () => {
<Menu>
<Menu.Item
onClick={async (e) => {
const title = uid();
const values = await FormDialog('修改列标题', () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
fieldName: {
type: 'string',
title: '原字段名称',
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
title: {
type: 'string',
title: '自定义列标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
}}
/>
</FormLayout>
);
}).open({
initialValues: {
fieldName: collectionField?.uiSchema?.title,
title: schema['title'],
},
});
const title = values.title || null;
field.title = title;
schema.title = title;
refresh();
@ -1420,8 +1673,9 @@ Table.Column.DesignableBar = () => {
setVisible(false);
}}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={async () => {
const s = remove();
@ -1430,7 +1684,7 @@ Table.Column.DesignableBar = () => {
await removeSchema(s);
}}
>
</Menu.Item>
</Menu>
}
@ -1453,14 +1707,14 @@ Table.SortHandle = observer((props: any) => {
Table.DesignableBar = observer((props) => {
const field = useField();
const { designable, schema, refresh, deepRemove } = useDesignable();
const { schema, refresh, deepRemove } = useDesignable();
const [visible, setVisible] = useState(false);
const { dragRef } = useContext(DraggableBlockContext);
if (!designable) {
return null;
}
const defaultPageSize =
schema['x-component-props']?.['pagination']?.['defaultPageSize'] || 20;
field?.componentProps?.pagination?.defaultPageSize || 10;
const collectionName = field?.componentProps?.collectionName;
const { fields } = useCollection({ collectionName });
console.log({ collectionName });
return (
<div className={cls('designable-bar', { active: visible })}>
<span
@ -1486,9 +1740,16 @@ Table.DesignableBar = observer((props) => {
const bool = !field.componentProps.showIndex;
schema['x-component-props']['showIndex'] = bool;
field.componentProps.showIndex = bool;
updateSchema(schema);
}}
>
{field.componentProps.showIndex ? '隐藏序号' : '显示序号'}
<div className={'nb-space-between'}>
{' '}
<Switch
size={'small'}
checked={field.componentProps.showIndex}
/>
</div>
</Menu.Item>
<Menu.Item
key={'dragSort'}
@ -1498,16 +1759,166 @@ Table.DesignableBar = observer((props) => {
: 'sort';
schema['x-component-props']['dragSort'] = dragSort;
field.componentProps.dragSort = dragSort;
updateSchema(schema);
}}
>
{field.componentProps.dragSort
? '禁用拖拽排序'
: '启用拖拽排序'}
<div className={'nb-space-between'}>
{' '}
<Switch
size={'small'}
checked={field.componentProps.dragSort}
/>
</div>
</Menu.Item>
{!field.componentProps.dragSort && (
<Menu.Item key={'defaultSort'}></Menu.Item>
<Menu.Item
key={'defaultSort'}
onClick={async () => {
const defaultSort =
field.componentProps?.defaultSort?.map(
(item: string) => {
return item.startsWith('-')
? {
field: item.substring(1),
direction: 'desc',
}
: {
field: item,
direction: 'asc',
};
},
);
const values = await FormDialog('设置默认排序', () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
defaultSort: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
properties: {
sort: {
type: 'void',
'x-decorator': 'FormItem',
'x-component':
'ArrayItems.SortHandle',
},
field: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
enum: fieldsToSortColumns(fields),
'x-component-props': {
style: {
width: 260,
},
},
},
direction: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
style: {
width: 100,
},
},
enum: [
{ label: '正序', value: 'asc' },
{
label: '倒序',
value: 'desc',
},
],
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component':
'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: '新增',
'x-component': 'ArrayItems.Addition',
},
},
},
},
}}
/>
</FormLayout>
);
}).open({
initialValues: {
defaultSort,
},
});
const sort = values.defaultSort.map((item) => {
return item.direction === 'desc'
? `-${item.field}`
: item.field;
});
schema['x-component-props']['defaultSort'] = sort;
field.componentProps.defaultSort = sort;
await updateSchema(schema);
console.log('defaultSort', sort);
}}
>
</Menu.Item>
)}
<Menu.Item key={'defaultFilter'}></Menu.Item>
<Menu.Item
key={'defaultFilter'}
onClick={async () => {
const { defaultFilter } = await FormDialog(
'设置筛选范围',
() => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
defaultFilter: {
type: 'object',
'x-component': 'Filter',
properties: fieldsToFilterColumns(fields),
},
},
}}
/>
</FormLayout>
);
},
).open({
initialValues: {
defaultFilter:
field?.componentProps?.defaultFilter || {},
},
});
schema['x-component-props']['defaultFilter'] =
defaultFilter;
field.componentProps.defaultFilter = defaultFilter;
await updateSchema(schema);
}}
>
</Menu.Item>
<Menu.Item key={'defaultPageSize'}>
{' '}
<Select
@ -1516,11 +1927,16 @@ Table.DesignableBar = observer((props) => {
onChange={(value) => {
const componentProps = schema['x-component-props'] || {};
set(componentProps, 'pagination.defaultPageSize', value);
set(componentProps, 'pagination.pageSize', value);
schema['x-component-props'] = componentProps;
field.componentProps.pagination.pageSize = value;
field.componentProps.pagination.defaultPageSize = value;
refresh();
updateSchema(schema);
}}
defaultValue={defaultPageSize}
>
<Select.Option value={10}>10</Select.Option>
<Select.Option value={20}>20</Select.Option>
<Select.Option value={50}>50</Select.Option>
<Select.Option value={100}>100</Select.Option>
@ -1539,7 +1955,7 @@ Table.DesignableBar = observer((props) => {
}
}}
>
</Menu.Item>
</Menu>
}

View File

@ -225,3 +225,9 @@ td.nb-table-operation {
min-height: 32px;
background-color: #e6f7ff;
}
.nb-space-between {
display: flex;
align-items: center;
justify-content: space-between;
}

View File

@ -1,7 +1,12 @@
import { observer, connect, useField, RecursionField } from '@formily/react';
import React from 'react';
import { Button, Tabs as AntdTabs, Dropdown, Menu, Switch } from 'antd';
import { findPropertyByPath, getSchemaPath, useDesignable } from '../../components/schema-renderer';
import {
findPropertyByPath,
getSchemaPath,
SchemaField,
useDesignable,
} from '../../components/schema-renderer';
import { Schema, SchemaKey } from '@formily/react';
import { PlusOutlined, MenuOutlined } from '@ant-design/icons';
import { useState } from 'react';
@ -11,6 +16,8 @@ import './style.less';
import { uid } from '@formily/shared';
import { DragHandle, SortableItem } from '../../components/Sortable';
import { DndContext, DragOverlay } from '@dnd-kit/core';
import { FormDialog, FormLayout } from '@formily/antd';
import IconPicker from '../../components/icon-picker';
const useTabs = ({ singleton }) => {
const tabsField = useField();
@ -38,7 +45,8 @@ const useTabs = ({ singleton }) => {
export const Tabs: any = observer((props: any) => {
const { singleton, ...others } = props;
const { schema, DesignableBar, appendChild, root, remove, insertAfter } = useDesignable();
const { schema, DesignableBar, appendChild, root, remove, insertAfter } =
useDesignable();
const tabs = useTabs({ singleton });
const [dragOverlayContent, setDragOverlayContent] = useState('');
@ -93,14 +101,45 @@ export const Tabs: any = observer((props: any) => {
type={'dashed'}
icon={<PlusOutlined />}
onClick={async () => {
const values = await FormDialog('新增标签页', () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
title: {
type: 'string',
title: '标签名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
icon: {
type: 'string',
title: '标签图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
}}
/>
</FormLayout>
);
}).open({
initialValues: {
title: schema['title'],
icon: schema['x-component-props']?.['icon'],
},
});
const data = appendChild({
type: 'void',
name: uid(),
title: uid(),
title: values.title,
'x-component': 'Tabs.TabPane',
'x-designable-bar': 'Tabs.TabPane.DesignableBar',
'x-component-props': {
tab: uid(),
icon: values.icon,
},
properties: {
[uid()]: {
@ -151,6 +190,7 @@ Tabs.TabPane = observer((props: any) => {
}}
>
<div className={'nb-tab-pane'}>
<IconPicker type={props.icon} />
{schema.title} <DesignableBar />
</div>
</SortableItem>
@ -185,10 +225,14 @@ Tabs.DesignableBar = () => {
schema['x-component-props'] || {};
schema['x-component-props'].singleton = singleton;
field.componentProps.singleton = singleton;
updateSchema(schema);
}}
>
<span style={{ marginRight: 24 }}></span>{' '}
<Switch size={'small'} checked={!!field.componentProps.singleton}/>
<Switch
size={'small'}
checked={!!field.componentProps.singleton}
/>
</Menu.Item>
</Menu>
}
@ -203,7 +247,7 @@ Tabs.DesignableBar = () => {
Tabs.TabPane.DesignableBar = () => {
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
const field = useField();
return (
<div className={cls('designable-bar', { active: visible })}>
<span
@ -222,12 +266,45 @@ Tabs.TabPane.DesignableBar = () => {
overlay={
<Menu>
<Menu.Item
onClick={() => {
schema.title = uid();
onClick={async () => {
const values = await FormDialog('修改名称和图标', () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
title: {
type: 'string',
title: '标签名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
icon: {
type: 'string',
title: '标签图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
}}
/>
</FormLayout>
);
}).open({
initialValues: {
title: schema['title'],
icon: schema['x-component-props']?.['icon'],
},
});
field.componentProps.icon = values.icon;
schema.title = values.title;
schema['x-component-props'] =
schema['x-component-props'] || {};
schema['x-component-props']['tab'] = uid();
schema['x-component-props']['icon'] = values.icon;
refresh();
updateSchema(schema);
}}
>
@ -240,7 +317,7 @@ Tabs.TabPane.DesignableBar = () => {
setVisible(false);
}}
>
</Menu.Item>
</Menu>
}

View File

@ -144,6 +144,7 @@ export class Table {
model,
fields = [],
indexes = [],
sortable,
} = options;
this.options = options;
this.database = database;
@ -155,9 +156,28 @@ export class Table {
this.addIndexes(indexes, 'modelOnly');
// this.modelInit('modelOnly');
this.setFields(fields);
this.initSortable();
database.runHooks('afterTableInit', this);
}
public initSortable() {
const { sortable } = this.options;
if (!sortable) {
return;
}
if (typeof sortable === 'string') {
this.addField({
type: 'sort',
name: sortable,
});
} else if (typeof sortable === 'object') {
this.addField({
...sortable,
type: 'sort',
});
}
}
public modelInit(reinitialize: Reinitialize = false) {
if (reinitialize || !this.Model) {
this.Model = this.defaultModel || class extends Model { };
@ -389,6 +409,7 @@ export class Table {
for (const key in fields) {
this.addField(fields[key], false);
}
this.initSortable();
// @ts-ignore
this.addIndexes(indexes, false);
this.modelInit(true);

View File

@ -4,11 +4,12 @@ export default {
name: 'collections',
title: '数据表配置',
model: 'Collection',
sortable: 'sort',
fields: [
{
type: 'sort',
name: 'sort',
},
// {
// type: 'sort',
// name: 'sort',
// },
{
type: 'uid',
name: 'name',
@ -24,6 +25,11 @@ export default {
type: 'string',
name: 'privilege',
},
{
type: 'json',
name: 'sortable',
defaultValue: 'sort',
},
{
type: 'json',
name: 'options',

View File

@ -5,12 +5,17 @@ export default {
name: 'fields',
title: '字段配置',
model: 'Field',
fields: [
{
sortable: {
type: 'sort',
name: 'sort',
scope: ['parentKey'],
},
fields: [
// {
// type: 'sort',
// name: 'sort',
// scope: ['parentKey'],
// },
{
type: 'uid',
name: 'key',

View File

@ -4,12 +4,17 @@ export default {
name: 'routes',
title: '路由表',
model: 'Route',
fields: [
{
sortable: {
type: 'sort',
name: 'sort',
scope: ['parentKey'],
},
fields: [
// {
// type: 'sort',
// name: 'sort',
// scope: ['parentKey'],
// },
{
type: 'uid',
name: 'key',

View File

@ -4,17 +4,22 @@ export default {
name: 'ui_schemas',
title: '字段配置',
model: 'UISchema',
sortable: {
type: 'sort',
name: 'sort',
scope: ['parentKey'],
},
fields: [
{
type: 'uid',
name: 'key',
primaryKey: true,
},
{
type: 'sort',
name: 'sort',
scope: ['parentKey'],
},
// {
// type: 'sort',
// name: 'sort',
// scope: ['parentKey'],
// },
{
type: 'string',
name: 'name',

View File

@ -8710,6 +8710,11 @@ flat-to-nested@^1.1.1:
resolved "https://registry.npmjs.org/flat-to-nested/-/flat-to-nested-1.1.1.tgz#ec183cd9a72f6bfbf8ca21acb0fc8235e509f581"
integrity sha512-Sym5oik6BO9JnsDEjv9Q9hPTCexG2ttk0UiM2mgLEiCiiUOQr8acBd33r8ixnoSGR0HAxPoP8WtLAL5oV46IhQ==
flat@^5.0.2:
version "5.0.2"
resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
flatted@^3.1.0:
version "3.1.1"
resolved "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"