mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-02 04:07:50 +08:00
feat: details block (#302)
This commit is contained in:
parent
111b9e67b0
commit
1f12c20838
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,7 +8,7 @@ yarn-error.log
|
||||
lerna-debug.log
|
||||
/.vscode
|
||||
/.idea
|
||||
db.sqlite
|
||||
*.sqlite
|
||||
coverage
|
||||
.umi
|
||||
/uploads
|
||||
|
@ -2,12 +2,14 @@ import React from 'react';
|
||||
import { SchemaComponentOptions } from '../schema-component/core/SchemaComponentOptions';
|
||||
import { RecordLink, useParamsFromRecord, useSourceIdFromParentRecord, useSourceIdFromRecord } from './BlockProvider';
|
||||
import { CalendarBlockProvider, useCalendarBlockProps } from './CalendarBlockProvider';
|
||||
import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvider';
|
||||
import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider';
|
||||
import * as bp from './hooks';
|
||||
import { KanbanBlockProvider, useKanbanBlockProps } from './KanbanBlockProvider';
|
||||
import { TableBlockProvider, useTableBlockProps } from './TableBlockProvider';
|
||||
import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider';
|
||||
import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider';
|
||||
|
||||
export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||
return (
|
||||
<SchemaComponentOptions
|
||||
@ -17,6 +19,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||
TableBlockProvider,
|
||||
TableSelectorProvider,
|
||||
FormBlockProvider,
|
||||
DetailsBlockProvider,
|
||||
KanbanBlockProvider,
|
||||
RecordLink,
|
||||
}}
|
||||
@ -27,6 +30,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||
useParamsFromRecord,
|
||||
useCalendarBlockProps,
|
||||
useFormBlockProps,
|
||||
useDetailsBlockProps,
|
||||
useTableFieldProps,
|
||||
useTableBlockProps,
|
||||
useTableSelectorProps,
|
||||
|
@ -0,0 +1,62 @@
|
||||
import { createForm } from '@formily/core';
|
||||
import { useField } from '@formily/react';
|
||||
import { Spin } from 'antd';
|
||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||
import { RecordProvider } from '../record-provider';
|
||||
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||
|
||||
export const DetailsBlockContext = createContext<any>({});
|
||||
|
||||
const InternalDetailsBlockProvider = (props) => {
|
||||
const { action, readPretty } = props;
|
||||
const field = useField<any>();
|
||||
const form = useMemo(
|
||||
() =>
|
||||
createForm({
|
||||
readPretty,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const { resource, service } = useBlockRequestContext();
|
||||
if (service.loading && !field.loaded) {
|
||||
return <Spin />;
|
||||
}
|
||||
field.loaded = true;
|
||||
return (
|
||||
<DetailsBlockContext.Provider
|
||||
value={{
|
||||
action,
|
||||
form,
|
||||
field,
|
||||
service,
|
||||
resource,
|
||||
}}
|
||||
>
|
||||
<RecordProvider record={service?.data?.data?.[0] || {}}>{props.children}</RecordProvider>
|
||||
</DetailsBlockContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const DetailsBlockProvider = (props) => {
|
||||
return (
|
||||
<BlockProvider {...props}>
|
||||
<InternalDetailsBlockProvider {...props} />
|
||||
</BlockProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useDetailsBlockContext = () => {
|
||||
return useContext(DetailsBlockContext);
|
||||
};
|
||||
|
||||
export const useDetailsBlockProps = () => {
|
||||
const ctx = useDetailsBlockContext();
|
||||
useEffect(() => {
|
||||
if (!ctx.service.loading) {
|
||||
ctx.form.setValues(ctx.service?.data?.data?.[0] || {});
|
||||
}
|
||||
}, [ctx.service.loading]);
|
||||
return {
|
||||
form: ctx.form,
|
||||
};
|
||||
};
|
@ -20,15 +20,16 @@ const useGroupField = (props) => {
|
||||
};
|
||||
|
||||
const InternalKanbanBlockProvider = (props) => {
|
||||
const field = useField();
|
||||
const field = useField<any>();
|
||||
const { resource, service } = useBlockRequestContext();
|
||||
const groupField = useGroupField(props);
|
||||
if (!groupField) {
|
||||
return null;
|
||||
}
|
||||
if (service.loading) {
|
||||
if (service.loading && !field.loaded) {
|
||||
return <Spin />;
|
||||
}
|
||||
field.loaded = true;
|
||||
return (
|
||||
<KanbanBlockContext.Provider
|
||||
value={{
|
||||
|
@ -5,6 +5,7 @@ import { useHistory } from 'react-router-dom';
|
||||
import { useCollection } from '../../collection-manager';
|
||||
import { useActionContext } from '../../schema-component';
|
||||
import { useBlockRequestContext, useFilterByTk } from '../BlockProvider';
|
||||
import { useDetailsBlockContext } from '../DetailsBlockProvider';
|
||||
import { TableFieldResource } from '../TableFieldProvider';
|
||||
|
||||
export const usePickActionProps = () => {
|
||||
@ -207,3 +208,23 @@ export const useBulkDestroyActionProps = () => {
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const useDetailsPaginationProps = () => {
|
||||
const ctx = useDetailsBlockContext();
|
||||
const count = ctx.service?.data?.meta?.count || 0;
|
||||
return {
|
||||
simple: true,
|
||||
hidden: count <= 1,
|
||||
current: ctx.service?.data?.meta?.page || 1,
|
||||
total: count,
|
||||
pageSize: 1,
|
||||
async onChange(page) {
|
||||
const params = ctx.service?.params?.[0];
|
||||
ctx.service.run({ ...params, page });
|
||||
},
|
||||
style: {
|
||||
marginTop: 24,
|
||||
textAlign: 'center',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -81,7 +81,8 @@ export const useSortFields = (collectionName: string) => {
|
||||
export const useCollectionFilterOptions = (collectionName: string) => {
|
||||
const { getCollectionFields, getInterface } = useCollectionManager();
|
||||
const fields = getCollectionFields(collectionName);
|
||||
const field2option = (field, nochildren) => {
|
||||
let depth = 0;
|
||||
const field2option = (field) => {
|
||||
if (!field.interface) {
|
||||
return;
|
||||
}
|
||||
@ -94,9 +95,15 @@ export const useCollectionFilterOptions = (collectionName: string) => {
|
||||
name: field.name,
|
||||
title: field?.uiSchema?.title || field.name,
|
||||
schema: field?.uiSchema,
|
||||
operators: operators || [],
|
||||
operators:
|
||||
operators?.filter?.((operator) => {
|
||||
return !operator?.visible || operator.visible(field);
|
||||
}) || [],
|
||||
};
|
||||
if (nochildren) {
|
||||
if (field.target && depth > 2) {
|
||||
return;
|
||||
}
|
||||
if (depth > 2) {
|
||||
return option;
|
||||
}
|
||||
if (children?.length) {
|
||||
@ -104,16 +111,17 @@ export const useCollectionFilterOptions = (collectionName: string) => {
|
||||
}
|
||||
if (nested) {
|
||||
const targetFields = getCollectionFields(field.target);
|
||||
const options = getOptions(targetFields, true);
|
||||
const options = getOptions(targetFields).filter(Boolean);
|
||||
option['children'] = option['children'] || [];
|
||||
option['children'].push(...options);
|
||||
}
|
||||
return option;
|
||||
};
|
||||
const getOptions = (fields, nochildren = false) => {
|
||||
const getOptions = (fields) => {
|
||||
++depth;
|
||||
const options = [];
|
||||
fields.forEach((field) => {
|
||||
const option = field2option(field, nochildren);
|
||||
const option = field2option(field);
|
||||
if (option) {
|
||||
options.push(option);
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import { ArrayItems } from '@formily/antd';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFormBlockContext } from '../../../block-provider';
|
||||
import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider';
|
||||
import { useCollection } from '../../../collection-manager';
|
||||
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
@ -53,3 +56,162 @@ export const ReadPrettyFormDesigner = () => {
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
||||
|
||||
export const DetailsDesigner = () => {
|
||||
const { name, title } = useCollection();
|
||||
const template = useSchemaTemplate();
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField();
|
||||
const dataSource = useCollectionFilterOptions(name);
|
||||
const { service } = useDetailsBlockContext();
|
||||
const { dn } = useDesignable();
|
||||
const sortFields = useSortFields(name);
|
||||
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
||||
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
|
||||
const sort = defaultSort?.map((item: string) => {
|
||||
return item.startsWith('-')
|
||||
? {
|
||||
field: item.substring(1),
|
||||
direction: 'desc',
|
||||
}
|
||||
: {
|
||||
field: item,
|
||||
direction: 'asc',
|
||||
};
|
||||
});
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set the data scope')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Set the data scope'),
|
||||
properties: {
|
||||
filter: {
|
||||
default: defaultFilter,
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
fieldSchema['x-decorator-props']['params'] = params;
|
||||
service.run({ ...service.params?.[0], filter });
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set default sorting rules')}
|
||||
components={{ ArrayItems }}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Set default sorting rules'),
|
||||
properties: {
|
||||
sort: {
|
||||
type: 'array',
|
||||
default: sort,
|
||||
'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',
|
||||
enum: sortFields,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
width: 260,
|
||||
},
|
||||
},
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-component-props': {
|
||||
optionType: 'button',
|
||||
},
|
||||
enum: [
|
||||
{
|
||||
label: t('ASC'),
|
||||
value: 'asc',
|
||||
},
|
||||
{
|
||||
label: t('DESC'),
|
||||
value: 'desc',
|
||||
},
|
||||
],
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
add: {
|
||||
type: 'void',
|
||||
title: t('Add sort field'),
|
||||
'x-component': 'ArrayItems.Addition',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ sort }) => {
|
||||
const sortArr = sort.map((item) => {
|
||||
return item.direction === 'desc' ? `-${item.field}` : item.field;
|
||||
});
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.sort = sortArr;
|
||||
field.decoratorProps.params = params;
|
||||
fieldSchema['x-decorator-props']['params'] = params;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
service.run({ ...service.params?.[0], sort: sortArr });
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.Template componentName={'Details'} collectionName={name} />
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Form as FormV2 } from './Form';
|
||||
import { FormDesigner, ReadPrettyFormDesigner } from './Form.Designer';
|
||||
import { DetailsDesigner, FormDesigner, ReadPrettyFormDesigner } from './Form.Designer';
|
||||
|
||||
FormV2.Designer = FormDesigner;
|
||||
FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner;
|
||||
|
||||
export { FormV2 };
|
||||
export { FormV2, DetailsDesigner };
|
||||
|
@ -22,6 +22,7 @@ export * from './kanban-v2';
|
||||
export * from './markdown';
|
||||
export * from './menu';
|
||||
export * from './page';
|
||||
export * from './pagination';
|
||||
export * from './password';
|
||||
export * from './radio';
|
||||
export * from './record-picker';
|
||||
@ -36,3 +37,4 @@ export * from './tree-select';
|
||||
export * from './upload';
|
||||
import './index.less';
|
||||
|
||||
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { observer } from '@formily/react';
|
||||
import { Pagination as AntdPagination } from 'antd';
|
||||
import React from 'react';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
|
||||
export const Pagination = observer((props: any) => {
|
||||
const { hidden, ...others } = useProps(props);
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
return <AntdPagination {...others} />;
|
||||
});
|
@ -23,6 +23,12 @@ export const BlockInitializers = {
|
||||
title: 'Form',
|
||||
component: 'FormBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'details',
|
||||
type: 'item',
|
||||
title: 'Details',
|
||||
component: 'DetailsBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'calendar',
|
||||
type: 'item',
|
||||
|
@ -0,0 +1,37 @@
|
||||
// 表单的操作配置
|
||||
export const DetailsActionInitializers = {
|
||||
title: '{{t("Configure actions")}}',
|
||||
icon: 'SettingOutlined',
|
||||
style: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Enable actions")}}',
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Edit")}}',
|
||||
component: 'UpdateActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Delete")}}',
|
||||
component: 'DestroyActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
@ -6,7 +6,7 @@ import { gridRowColWrap } from '../utils';
|
||||
const useRelationFields = () => {
|
||||
const { fields } = useCollection();
|
||||
return fields
|
||||
.filter((field) => field.interface === 'linkTo')
|
||||
.filter((field) => ['linkTo', 'subTable'].includes(field.interface))
|
||||
.map((field) => {
|
||||
return {
|
||||
key: field.name,
|
||||
|
@ -1,6 +1,7 @@
|
||||
export * from './BlockInitializers';
|
||||
export * from './CalendarActionInitializers';
|
||||
export * from './CreateFormBlockInitializers';
|
||||
export * from './DetailsActionInitializers';
|
||||
export * from './FormActionInitializers';
|
||||
export * from './FormItemInitializers';
|
||||
export * from './ReadPrettyFormActionInitializers';
|
||||
|
@ -12,6 +12,7 @@ import { useSchemaTemplateManager } from '../../schema-templates';
|
||||
import { SchemaInitializer } from '../SchemaInitializer';
|
||||
import {
|
||||
createCalendarBlockSchema,
|
||||
createDetailsBlockSchema,
|
||||
createFormBlockSchema,
|
||||
createKanbanBlockSchema,
|
||||
createReadPrettyFormBlockSchema,
|
||||
@ -125,6 +126,23 @@ export const FormBlockInitializer = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DetailsBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
const { getCollection } = useCollectionManager();
|
||||
return (
|
||||
<DataBlockInitializer
|
||||
{...props}
|
||||
icon={<TableOutlined />}
|
||||
componentType={'Details'}
|
||||
onCreateBlockSchema={async ({ item }) => {
|
||||
const collection = getCollection(item.name);
|
||||
const schema = createDetailsBlockSchema({ collection: item.name, rowKey: collection.filterTargetKey || 'id' });
|
||||
insert(schema);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalendarBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
const { getCollection } = useCollectionManager();
|
||||
|
@ -283,6 +283,76 @@ export const useCollectionDataSourceItems = (componentName) => {
|
||||
];
|
||||
};
|
||||
|
||||
export const createDetailsBlockSchema = (options) => {
|
||||
const {
|
||||
formItemInitializers = 'ReadPrettyFormItemInitializers',
|
||||
actionInitializers = 'DetailsActionInitializers',
|
||||
collection,
|
||||
association,
|
||||
resource,
|
||||
template,
|
||||
...others
|
||||
} = options;
|
||||
const resourceName = resource || association || collection;
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
'x-acl-action': `${resourceName}:get`,
|
||||
'x-decorator': 'DetailsBlockProvider',
|
||||
'x-decorator-props': {
|
||||
resource: resourceName,
|
||||
collection,
|
||||
association,
|
||||
readPretty: true,
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 1,
|
||||
},
|
||||
// useParams: '{{ useParamsFromRecord }}',
|
||||
...others,
|
||||
},
|
||||
'x-designer': 'DetailsDesigner',
|
||||
'x-component': 'CardItem',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-read-pretty': true,
|
||||
'x-component-props': {
|
||||
useProps: '{{ useDetailsBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-initializer': actionInitializers,
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
grid: template || {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': formItemInitializers,
|
||||
properties: {},
|
||||
},
|
||||
pagination: {
|
||||
type: 'void',
|
||||
'x-component': 'Pagination',
|
||||
'x-component-props': {
|
||||
useProps: '{{ useDetailsPaginationProps }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
console.log(JSON.stringify(schema, null, 2));
|
||||
return schema;
|
||||
};
|
||||
|
||||
export const createFormBlockSchema = (options) => {
|
||||
const {
|
||||
formItemInitializers = 'FormItemInitializers',
|
||||
|
Loading…
Reference in New Issue
Block a user