feat(collection-manager): support for setting the title field (#1729)

* feat: configure fields

* feat: edit

* feat: enable delete action

* style: indent style

* feat: support to override and view inherited fields

* feat: support to delete and add field and sync

* fix: should delete the seleted option

* feat: support to setting title field

* feat: avoid infinite loops and add default title field

* feat(data-templates): use titleField to display title

* fix: fix the pagination of collection manager

* feat: add prompt for title field switch

* feat: filter field types

* feat: not use title switch when field interface is icon

* feat: translate

* fix: sync update collections data

* feat: translate

* fix: fix build error

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
被雨水过滤的空气-Rairn 2023-04-21 11:19:59 +08:00 committed by GitHub
parent 5930218811
commit 3c884cdd36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 561 additions and 43 deletions

View File

@ -10,7 +10,7 @@ import * as defaultInterfaces from './interfaces';
import { CollectionManagerOptions } from './types'; import { CollectionManagerOptions } from './types';
export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (props) => { export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (props) => {
const { service, interfaces, collections = [], refreshCM, templates } = props; const { service, interfaces, collections = [], refreshCM, updateCollection, templates } = props;
const defaultTemplates = keyBy(templateOptions(), (item) => item.name); const defaultTemplates = keyBy(templateOptions(), (item) => item.name);
const ctx = useContext(CollectionManagerContext); const ctx = useContext(CollectionManagerContext);
return ( return (
@ -22,6 +22,7 @@ export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (pr
templates: { ...defaultTemplates, ...templates }, templates: { ...defaultTemplates, ...templates },
collections: [...ctx.collections, ...collections], collections: [...ctx.collections, ...collections],
refreshCM, refreshCM,
updateCollection,
}} }}
> >
<CollectionManagerSchemaComponentProvider>{props.children}</CollectionManagerSchemaComponentProvider> <CollectionManagerSchemaComponentProvider>{props.children}</CollectionManagerSchemaComponentProvider>
@ -76,12 +77,17 @@ export const RemoteCollectionManagerProvider = (props: any) => {
return data?.data || []; return data?.data || [];
}; };
const updateCollection = (collection) => {
service.mutate({ data: collection });
};
return ( return (
<CollectionCategroriesProvider service={{ ...result }} refreshCategory={refreshCategory}> <CollectionCategroriesProvider service={{ ...result }} refreshCategory={refreshCategory}>
<CollectionManagerProvider <CollectionManagerProvider
service={{ ...service, contentLoading, setContentLoading }} service={{ ...service, contentLoading, setContentLoading }}
collections={service?.data?.data} collections={service?.data?.data}
refreshCM={refreshCM} refreshCM={refreshCM}
updateCollection={updateCollection}
{...props} {...props}
/> />
</CollectionCategroriesProvider> </CollectionCategroriesProvider>
@ -95,7 +101,7 @@ export const CollectionCategroriesProvider = (props) => {
value={{ value={{
data: service?.data?.data, data: service?.data?.data,
refresh: refreshCategory, refresh: refreshCategory,
...props ...props,
}} }}
> >
{props.children} {props.children}

View File

@ -0,0 +1,440 @@
import { css } from '@emotion/css';
import { Field, createForm } from '@formily/core';
import { FieldContext, FormContext, useField } from '@formily/react';
import { Space, Switch, Table, TableColumnProps, Tag, Tooltip } from 'antd';
import React, { useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { RecordProvider, useRecord } from '../../record-provider';
import { Action, useAttach, useCompile } from '../../schema-component';
import {
ResourceActionContext,
ResourceActionProvider,
useResourceActionContext,
useResourceContext,
} from '../ResourceActionProvider';
import {
isDeleteButtonDisabled,
useBulkDestroyActionAndRefreshCM,
useDestroyActionAndRefreshCM,
} from '../action-hooks';
import { useCollectionManager } from '../hooks/useCollectionManager';
import { AddCollectionField } from './AddFieldAction';
import { EditCollectionField } from './EditFieldAction';
import { OverridingCollectionField } from './OverridingCollectionField';
import { SyncFieldsAction } from './SyncFieldsAction';
import { ViewCollectionField } from './ViewInheritedField';
import { collection } from './schemas/collectionFields';
const CELL_WIDTH = 200;
const indentStyle = css`
.ant-table {
margin-left: -7px !important;
}
`;
const rowStyle = css`
.ant-table-cell {
background-color: white;
}
`;
const titlePrompt = 'Default title for each record';
// 只有下面类型的字段才可以设置为标题字段
const expectTypes = ['string', 'integer', 'bigInt', 'float', 'double', 'decimal', 'date', 'dateonly', 'time'];
const excludeInterfaces = ['icon'];
const CurrentFields = (props) => {
const compile = useCompile();
const { getInterface } = useCollectionManager();
const { t } = useTranslation();
const { setState } = useResourceActionContext();
const { resource, targetKey } = props.collectionResource || {};
const { [targetKey]: filterByTk, titleField } = useRecord();
const [loadingRecord, setLoadingRecord] = React.useState<any>(null);
const { updateCollection } = useCollectionManager();
const columns: TableColumnProps<any>[] = [
{
dataIndex: ['uiSchema', 'title'],
title: t('Field display name'),
render: (value) => <div style={{ marginLeft: 7 }}>{compile(value)}</div>,
},
{
dataIndex: 'name',
title: t('Field name'),
width: CELL_WIDTH + 20,
},
{
dataIndex: 'interface',
title: t('Field interface'),
width: CELL_WIDTH,
render: (value) => <Tag>{compile(getInterface(value)?.title)}</Tag>,
},
{
dataIndex: 'titleField',
title: t('Title field'),
width: CELL_WIDTH,
render: function Render(_, record) {
const handleChange = (checked) => {
setLoadingRecord(record);
resource
.update({ filterByTk, values: { titleField: checked ? record.name : 'id' } })
.then(async () => {
const data = await props.refreshAsync();
if (data?.data) {
updateCollection(data.data);
}
setLoadingRecord(null);
})
.catch((err) => {
setLoadingRecord(null);
console.error(err);
});
};
return expectTypes.includes(record.type) && !excludeInterfaces.includes(record.interface) ? (
<Tooltip title={t(titlePrompt)} placement="right" overlayInnerStyle={{ textAlign: 'center' }}>
<Switch
size="small"
loading={record.name === loadingRecord?.name}
checked={record.name === (titleField || 'id')}
onChange={handleChange}
/>
</Tooltip>
) : null;
},
},
{
dataIndex: 'actions',
title: t('Actions'),
width: CELL_WIDTH,
render: (_, record) => {
const deleteProps = {
confirm: {
title: t('Delete record'),
content: t('Are you sure you want to delete it?'),
},
useAction: useDestroyActionAndRefreshCM,
disabled: isDeleteButtonDisabled(record),
title: t('Delete'),
};
return (
<RecordProvider record={record}>
<Space>
<EditCollectionField type="primary" />
<Action.Link {...deleteProps} />
</Space>
</RecordProvider>
);
},
},
];
return (
<Table
rowKey={'name'}
columns={columns}
showHeader={false}
pagination={false}
dataSource={props.fields}
rowSelection={{
type: 'checkbox',
onChange: (selectedRowKeys) => {
setState((state) => {
const result = [...(state.selectedRowKeys || []), ...selectedRowKeys];
return {
...state,
selectedRowKeys: result,
};
});
},
}}
className={indentStyle}
/>
);
};
const InheritFields = (props) => {
const compile = useCompile();
const { getInterface } = useCollectionManager();
const { resource, targetKey } = props.collectionResource || {};
const { [targetKey]: filterByTk, titleField, name } = useRecord();
const [loadingRecord, setLoadingRecord] = React.useState(null);
const { t } = useTranslation();
const { updateCollection } = useCollectionManager();
const columns: TableColumnProps<any>[] = [
{
dataIndex: ['uiSchema', 'title'],
title: t('Field display name'),
render: (value) => <div style={{ marginLeft: 1 }}>{compile(value)}</div>,
},
{
dataIndex: 'name',
title: t('Field name'),
width: CELL_WIDTH + 20,
},
{
dataIndex: 'interface',
title: t('Field interface'),
width: CELL_WIDTH,
render: (value) => <Tag>{compile(getInterface(value)?.title)}</Tag>,
},
{
dataIndex: 'titleField',
title: t('Title field'),
width: CELL_WIDTH,
render(_, record) {
const handleChange = (checked) => {
setLoadingRecord(record);
resource
.update({ filterByTk, values: { titleField: checked ? record.name : 'id' } })
.then(async () => {
const data = await props.refreshAsync();
if (data?.data) {
updateCollection(data.data);
}
setLoadingRecord(null);
})
.catch((err) => {
setLoadingRecord(null);
console.error(err);
});
};
return expectTypes.includes(record.type) && !excludeInterfaces.includes(record.interface) ? (
<Tooltip title={t(titlePrompt)} placement="right" overlayInnerStyle={{ textAlign: 'center' }}>
<Switch
size="small"
loading={record.name === loadingRecord?.name}
checked={record.name === (titleField || 'id')}
onChange={handleChange}
/>
</Tooltip>
) : null;
},
},
{
dataIndex: 'actions',
title: t('Actions'),
width: CELL_WIDTH,
render: function Render(_, record) {
const overrideProps = {
type: 'primary',
currentCollection: name,
};
const viewCollectionProps = {
type: 'primary',
};
return (
<RecordProvider record={record}>
<Space>
<OverridingCollectionField {...overrideProps} />
<ViewCollectionField {...viewCollectionProps} />
</Space>
</RecordProvider>
);
},
},
];
return (
<Table
rowKey={'name'}
columns={columns}
showHeader={false}
pagination={false}
dataSource={props.fields.filter((field) => field.interface)}
/>
);
};
export const CollectionFields = (props) => {
const compile = useCompile();
const field = useField<Field>();
const { name } = useRecord();
const { getInterface, getInheritCollections, getCollection, getCurrentCollectionFields } = useCollectionManager();
const form = useMemo(() => createForm(), []);
const f = useAttach(form.createArrayField({ ...field.props, basePath: '' }));
const { t } = useTranslation();
const collectionResource = useResourceContext();
const { refreshAsync } = useContext(ResourceActionContext);
const inherits = getInheritCollections(name);
const columns: TableColumnProps<any>[] = [
{
dataIndex: 'title',
title: t('Field display name'),
render: (value) => (
<div
className={css`
font-weight: 500;
white-space: nowrap;
width: 100px;
`}
>
{value}
</div>
),
// width: CELL_WIDTH,
},
{
dataIndex: 'name',
title: t('Field name'),
width: CELL_WIDTH + 20,
},
{
dataIndex: 'interface',
title: t('Field interface'),
width: CELL_WIDTH,
},
{
dataIndex: 'titleField',
title: t('Title field'),
width: CELL_WIDTH,
},
{
dataIndex: 'actions',
title: t('Actions'),
width: CELL_WIDTH,
},
];
const fields = getCurrentCollectionFields(name);
const groups = {
pf: [],
association: [],
general: [],
system: [],
};
fields.forEach((field) => {
if (field.primaryKey || field.isForeignKey) {
groups.pf.push(field);
} else if (field.interface) {
const conf = getInterface(field.interface);
if (conf.group === 'systemInfo') {
groups.system.push(field);
} else if (['m2m', 'm2o', 'o2b', 'o2m', 'linkTo'].includes(field.interface)) {
groups.association.push(field);
} else {
groups.general.push(field);
}
}
});
const dataSource = [
{
key: 'pf',
title: t('PK & FK fields'),
fields: groups.pf,
},
{
key: 'association',
title: t('Association fields'),
fields: groups.association,
},
{
key: 'general',
title: t('General fields'),
fields: groups.general,
},
{
key: 'system',
title: t('System fields'),
fields: groups.system,
},
];
dataSource.push(
...inherits.map((key) => {
const collection = getCollection(key);
return {
key,
title: `${t('Inherited fields')} - ` + compile(collection.title),
inherit: true,
fields: collection.fields,
};
}),
);
const resourceActionProps = {
association: {
sourceKey: 'name',
targetKey: 'name',
},
collection,
request: {
resource: 'collections.fields',
action: 'list',
params: {
paginate: false,
filter: {
$or: [{ 'interface.$not': null }, { 'options.source.$notEmpty': true }],
},
sort: ['sort'],
},
},
};
const deleteProps = useMemo(
() => ({
useAction: useBulkDestroyActionAndRefreshCM,
title: t('Delete'),
icon: 'DeleteOutlined',
confirm: {
title: t('Delete record'),
content: t('Are you sure you want to delete it?'),
},
}),
[t],
);
const addProps = { type: 'primary' };
const syncProps = { type: 'primary' };
return (
<ResourceActionProvider {...resourceActionProps}>
<FormContext.Provider value={form}>
<FieldContext.Provider value={f}>
<Space
align={'end'}
className={css`
justify-content: flex-end;
display: flex;
margin-bottom: 16px;
`}
>
<Action {...deleteProps} />
<SyncFieldsAction {...syncProps} />
<AddCollectionField {...addProps} />
</Space>
<Table
rowKey={'key'}
columns={columns}
dataSource={dataSource.filter((d) => d.fields.length)}
pagination={false}
expandable={{
defaultExpandAllRows: true,
expandedRowClassName: () => rowStyle,
expandedRowRender: (record) =>
record.inherit ? (
<InheritFields
fields={record.fields}
collectionResource={collectionResource}
refreshAsync={refreshAsync}
/>
) : (
<CurrentFields
fields={record.fields}
collectionResource={collectionResource}
refreshAsync={refreshAsync}
/>
),
}}
/>
</FieldContext.Provider>
</FormContext.Provider>
</ResourceActionProvider>
);
};

View File

@ -14,7 +14,7 @@ import {
useCompile, useCompile,
useRecord, useRecord,
useRequest, useRequest,
useSchemaInitializer useSchemaInitializer,
} from '../..'; } from '../..';
import { overridingSchema } from '../Configuration/schemas/collectionFields'; import { overridingSchema } from '../Configuration/schemas/collectionFields';
@ -192,10 +192,9 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
}); });
}; };
const ExpandedRowRender = (record: CategorizeDataItem, index, indent, expanded) => {
const expandedRowRender = (record: CategorizeDataItem, index, indent, expanded) => { const columns = useTableColumns();
if(!props.loading){ if (!props.loading) {
const columns = useTableColumns();
if (inherits.includes(record.key)) { if (inherits.includes(record.key)) {
columns.pop(); columns.pop();
columns.push({ columns.push({
@ -244,7 +243,6 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
/> />
); );
} }
}; };
return ( return (
<div <div
@ -262,7 +260,7 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
dataSource={categorizeData} dataSource={categorizeData}
pagination={false} pagination={false}
expandable={{ expandable={{
expandedRowRender, expandedRowRender: ExpandedRowRender,
expandedRowKeys: expandedKeys, expandedRowKeys: expandedKeys,
}} }}
onExpand={(expanded, record) => { onExpand={(expanded, record) => {

View File

@ -12,10 +12,11 @@ import { CollectionCategroriesContext } from '../context';
import { useCollectionManager } from '../hooks/useCollectionManager'; import { useCollectionManager } from '../hooks/useCollectionManager';
import { DataSourceContext } from '../sub-table'; import { DataSourceContext } from '../sub-table';
import { AddSubFieldAction } from './AddSubFieldAction'; import { AddSubFieldAction } from './AddSubFieldAction';
import { CollectionFields } from './CollectionFields';
import { EditSubFieldAction } from './EditSubFieldAction'; import { EditSubFieldAction } from './EditSubFieldAction';
import { FieldSummary } from './components/FieldSummary'; import { FieldSummary } from './components/FieldSummary';
import { TemplateSummay } from './components/TemplateSummay';
import { collectionSchema } from './schemas/collections'; import { collectionSchema } from './schemas/collections';
import {TemplateSummay} from './components/TemplateSummay';
/** /**
* @param service * @param service
@ -166,6 +167,7 @@ export const ConfigurationTable = () => {
FieldSummary, FieldSummary,
TemplateSummay, TemplateSummay,
CollectionFieldsTable, CollectionFieldsTable,
CollectionFields,
}} }}
scope={{ scope={{
useDestroySubField, useDestroySubField,

View File

@ -9,14 +9,15 @@ import {
useSensor, useSensor,
useSensors useSensors
} from '@dnd-kit/core'; } from '@dnd-kit/core';
import { observer, RecursionField } from '@formily/react'; import { RecursionField, observer } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Badge, Card, Dropdown, Menu, Modal, Tabs } from 'antd'; import { Badge, Card, Dropdown, Menu, Modal, Tabs } from 'antd';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { useAPIClient } from '../../api-client'; import { useAPIClient } from '../../api-client';
import { SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component'; import { SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component';
import { CollectionCategroriesContext } from '../context';
import { useResourceActionContext } from '../ResourceActionProvider'; import { useResourceActionContext } from '../ResourceActionProvider';
import { CollectionCategroriesContext } from '../context';
import { CollectionFields } from './CollectionFields';
import { collectionTableSchema } from './schemas/collections'; import { collectionTableSchema } from './schemas/collections';
function Draggable(props) { function Draggable(props) {
@ -240,6 +241,7 @@ export const ConfigurationTabs = () => {
> >
<Card bordered={false}> <Card bordered={false}>
<SchemaComponentOptions <SchemaComponentOptions
components={{ CollectionFields }}
inherit inherit
scope={{ loadCategories, categoryVisible: item.id === 'all', categoryId: item.id }} scope={{ loadCategories, categoryVisible: item.id === 'all', categoryId: item.id }}
> >

View File

@ -8,10 +8,10 @@ import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client'; import { useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import { useCancelAction } from '../action-hooks'; import { useCancelAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components'; import * as components from './components';
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => { const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
@ -26,10 +26,10 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
name: { name: {
'x-component': 'CollectionField', 'x-component': 'CollectionField',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-disabled': true, // 'x-disabled': true,
}, },
}; };
properties.name['x-disabled'] = true; // properties.name['x-disabled'] = true;
if (schema.hasDefaultValue === true) { if (schema.hasDefaultValue === true) {
properties['defaultValue'] = cloneDeep(schema.default.uiSchema); properties['defaultValue'] = cloneDeep(schema.default.uiSchema);
properties['defaultValue']['title'] = compile('{{ t("Default value") }}'); properties['defaultValue']['title'] = compile('{{ t("Default value") }}');

View File

@ -8,10 +8,10 @@ import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import { useCancelAction, useUpdateAction } from '../action-hooks'; import { useCancelAction, useUpdateAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components'; import * as components from './components';
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => { const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
@ -24,7 +24,7 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
} }
if (schema.hasDefaultValue === true) { if (schema.hasDefaultValue === true) {
properties['defaultValue'] = cloneDeep(schema.default.uiSchema)||{}; properties['defaultValue'] = cloneDeep(schema.default.uiSchema) || {};
properties['defaultValue']['title'] = compile('{{ t("Default value") }}'); properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
properties['defaultValue']['x-decorator'] = 'FormItem'; properties['defaultValue']['x-decorator'] = 'FormItem';
properties['defaultValue']['x-reactions'] = { properties['defaultValue']['x-reactions'] = {

View File

@ -8,10 +8,10 @@ import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import { useCancelAction } from '../action-hooks'; import { useCancelAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components'; import * as components from './components';
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => { const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {

View File

@ -2,7 +2,7 @@ import { ISchema } from '@formily/react';
import { CollectionOptions } from '../../types'; import { CollectionOptions } from '../../types';
import { CollectionFieldInterface } from '../components/CollectionFieldInterface'; import { CollectionFieldInterface } from '../components/CollectionFieldInterface';
const collection: CollectionOptions = { export const collection: CollectionOptions = {
name: 'fields', name: 'fields',
fields: [ fields: [
{ {

View File

@ -1,14 +1,12 @@
import { ISchema, Schema } from '@formily/react'; import { ISchema, Schema } from '@formily/react';
import { message } from 'antd';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { message } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../../api-client'; import { useAPIClient } from '../../../api-client';
import { i18n } from '../../../i18n'; import { i18n } from '../../../i18n';
import { CollectionOptions } from '../../types'; import { CollectionOptions } from '../../types';
import { CollectionTemplate } from '../components/CollectionTemplate';
import { CollectionCategory } from '../components/CollectionCategory'; import { CollectionCategory } from '../components/CollectionCategory';
import { collectionFieldSchema } from './collectionFields'; import { CollectionTemplate } from '../components/CollectionTemplate';
const compile = (source) => { const compile = (source) => {
return Schema.compile(source, { t: i18n.t }); return Schema.compile(source, { t: i18n.t });
}; };
@ -255,6 +253,7 @@ export const collectionTableSchema: ISchema = {
'x-component': 'Action.Drawer', 'x-component': 'Action.Drawer',
'x-component-props': { 'x-component-props': {
destroyOnClose: true, destroyOnClose: true,
width: '70%',
}, },
'x-reactions': (field) => { 'x-reactions': (field) => {
const i = field.path.segments[1]; const i = field.path.segments[1];
@ -265,7 +264,10 @@ export const collectionTableSchema: ISchema = {
} }
}, },
properties: { properties: {
collectionFieldSchema, collectionFieldSchema: {
type: 'void',
'x-component': 'CollectionFields',
},
}, },
}, },
}, },

View File

@ -202,7 +202,7 @@ export const useCollectionFilterOptions = (collectionName: string) => {
export const useLinkageCollectionFilterOptions = (collectionName: string) => { export const useLinkageCollectionFilterOptions = (collectionName: string) => {
const { getCollectionFields, getInterface } = useCollectionManager(); const { getCollectionFields, getInterface } = useCollectionManager();
const fields = getCollectionFields(collectionName).filter((v)=>!['o2m', 'm2m'].includes(v.interface)) const fields = getCollectionFields(collectionName).filter((v) => !['o2m', 'm2m'].includes(v.interface));
const field2option = (field, depth) => { const field2option = (field, depth) => {
if (!field.interface) { if (!field.interface) {
return; return;
@ -232,7 +232,7 @@ export const useLinkageCollectionFilterOptions = (collectionName: string) => {
option['children'] = children; option['children'] = children;
} }
if (nested) { if (nested) {
const targetFields = getCollectionFields(field.target).filter((v)=>!['o2m', 'm2m'].includes(v.interface)) const targetFields = getCollectionFields(field.target).filter((v) => !['o2m', 'm2m'].includes(v.interface));
const options = getOptions(targetFields, depth + 1).filter(Boolean); const options = getOptions(targetFields, depth + 1).filter(Boolean);
option['children'] = option['children'] || []; option['children'] = option['children'] || [];
option['children'].push(...options); option['children'].push(...options);
@ -253,7 +253,6 @@ export const useLinkageCollectionFilterOptions = (collectionName: string) => {
return options; return options;
}; };
export const useFilterDataSource = (options) => { export const useFilterDataSource = (options) => {
const { name } = useCollection(); const { name } = useCollection();
const data = useCollectionFilterOptions(name); const data = useCollectionFilterOptions(name);
@ -378,7 +377,7 @@ export const useDestroyAction = () => {
export const useBulkDestroyAction = () => { export const useBulkDestroyAction = () => {
const { state, setState, refresh } = useResourceActionContext(); const { state, setState, refresh } = useResourceActionContext();
const { resource, targetKey } = useResourceContext(); const { resource } = useResourceContext();
return { return {
async run() { async run() {
await resource.destroy({ await resource.destroy({
@ -431,8 +430,13 @@ export const useDestroyActionAndRefreshCM = () => {
}; };
}; };
export const useDeleteButtonDisabled = () => { export const useDeleteButtonDisabled = (record?: any) => {
const { interface: i, deletable = true } = useRecord(); const recordFromProvider = useRecord();
return isDeleteButtonDisabled(record || recordFromProvider);
};
export const isDeleteButtonDisabled = (record?: any) => {
const { interface: i, deletable = true } = record || {};
return !deletable || i === 'id'; return !deletable || i === 'id';
}; };

View File

@ -7,7 +7,8 @@ import { CollectionManagerContext } from '../context';
import { CollectionFieldOptions } from '../types'; import { CollectionFieldOptions } from '../types';
export const useCollectionManager = () => { export const useCollectionManager = () => {
const { refreshCM, service, interfaces, collections, templates } = useContext(CollectionManagerContext); const { refreshCM, updateCollection, service, interfaces, collections, templates } =
useContext(CollectionManagerContext);
const compile = useCompile(); const compile = useCompile();
const getInheritedFields = (name) => { const getInheritedFields = (name) => {
const inheritKeys = getInheritCollections(name); const inheritKeys = getInheritCollections(name);
@ -241,6 +242,7 @@ export const useCollectionManager = () => {
getInheritCollections, getInheritCollections,
getChildrenCollections, getChildrenCollections,
refreshCM: () => refreshCM?.(), refreshCM: () => refreshCM?.(),
updateCollection,
get(name: string) { get(name: string) {
return collections?.find((collection) => collection.name === name); return collections?.find((collection) => collection.name === name);
}, },

View File

@ -6,6 +6,7 @@ export interface CollectionManagerOptions {
collections?: any[]; collections?: any[];
templates?: any; templates?: any;
refreshCM?: () => Promise<void>; refreshCM?: () => Promise<void>;
updateCollection?: (collection: any) => void;
} }
export interface FieldOptions { export interface FieldOptions {

View File

@ -213,6 +213,7 @@ export default {
"Optional fields": "Optional fields", "Optional fields": "Optional fields",
"System fields": "System fields", "System fields": "System fields",
"General fields": "General fields", "General fields": "General fields",
"Inherited fields": "Inherited fields",
"Parent collection fields": "Parent collection fields", "Parent collection fields": "Parent collection fields",
"Basic": "Basic", "Basic": "Basic",
"Single line text": "Single line text", "Single line text": "Single line text",

View File

@ -595,4 +595,14 @@ export default {
'Click or drag file to this area to upload': 'クリックまたはドラッグしてファイルをアップロード', 'Click or drag file to this area to upload': 'クリックまたはドラッグしてファイルをアップロード',
'Support for a single or bulk upload, file size should not exceed': '単一または複数のファイルをアップロードできます。ファイルサイズは', 'Support for a single or bulk upload, file size should not exceed': '単一または複数のファイルをアップロードできます。ファイルサイズは',
'Default title for each record': '各レコードのデフォルトタイトル',
'If collection inherits, choose inherited collections as templates': 'コレクションが継承されている場合、継承されたコレクションをテンプレートとして選択してください',
'Select an existing piece of data as the initialization data for the form': '既存のデータを選択して、フォームの初期化データとして使用します',
'Only the selected fields will be used as the initialization data for the form': '選択したフィールドのみがフォームの初期化データとして使用されます',
'Template Data': 'テンプレートデータ',
'Data fields': 'データフィールド',
'Add template': 'テンプレートを追加',
'Display data template selector': 'データテンプレートセレクターを表示',
'Form data templates': 'フォームデータテンプレート',
} }

View File

@ -173,6 +173,7 @@ export default {
"Optional fields": "Campos opcionais", "Optional fields": "Campos opcionais",
"System fields": "Campos do sistema", "System fields": "Campos do sistema",
"General fields": "Campos gerais", "General fields": "Campos gerais",
"Inherited fields": "Campos herdados",
"Parent collection fields": "Campos da coleção pai", "Parent collection fields": "Campos da coleção pai",
"Basic": "Básico", "Basic": "Básico",
"Single line text": "Texto de uma linha", "Single line text": "Texto de uma linha",
@ -647,4 +648,14 @@ export default {
'Click or drag file to this area to upload': 'Clique ou arraste o arquivo para esta área para fazer o upload', 'Click or drag file to this area to upload': 'Clique ou arraste o arquivo para esta área para fazer o upload',
'Support for a single or bulk upload, file size should not exceed': 'Suporte para upload único ou em massa, o tamanho do arquivo não deve exceder', 'Support for a single or bulk upload, file size should not exceed': 'Suporte para upload único ou em massa, o tamanho do arquivo não deve exceder',
'Default title for each record': 'Título padrão para cada registro',
'If collection inherits, choose inherited collections as templates': 'Se a coleção herda, escolha as coleções herdadas como modelos',
'Select an existing piece of data as the initialization data for the form': 'Selecione um pedaço de dados existente como os dados de inicialização para o formulário',
'Only the selected fields will be used as the initialization data for the form': 'Somente os campos selecionados serão usados como dados de inicialização para o formulário',
'Template Data': 'Dados do modelo',
'Data fields': 'Campos de dados',
'Add template': 'Adicionar modelo',
'Display data template selector': 'Exibir seletor de modelo de dados',
'Form data templates': 'Modelos de dados do formulário',
}; };

View File

@ -502,4 +502,14 @@ export default {
'Click or drag file to this area to upload': "Нажмите или перетащите файл в эту область, чтобы загрузить", 'Click or drag file to this area to upload': "Нажмите или перетащите файл в эту область, чтобы загрузить",
'Support for a single or bulk upload, file size should not exceed': "Поддержка одиночной или массовой загрузки, размер файла не должен превышать", 'Support for a single or bulk upload, file size should not exceed': "Поддержка одиночной или массовой загрузки, размер файла не должен превышать",
'Default title for each record': "Заголовок по умолчанию для каждой записи",
'If collection inherits, choose inherited collections as templates': "Если коллекция наследуется, выберите наследуемые коллекции в качестве шаблонов",
'Select an existing piece of data as the initialization data for the form': "Выберите существующие данные в качестве исходных данных для формы",
'Only the selected fields will be used as the initialization data for the form': "Только выбранные поля будут использоваться в качестве исходных данных для формы",
'Template Data': "Шаблон данных",
'Data fields': "Поля данных",
'Add template': "Добавить шаблон",
'Display data template selector': "Отображать селектор шаблона данных",
'Form data templates': "Шаблоны данных формы",
} }

View File

@ -174,6 +174,7 @@ export default {
"Field display name": "Alan görünen adı", "Field display name": "Alan görünen adı",
"Field type": "Alan türü", "Field type": "Alan türü",
"Field interface": "Alan arayüzü", "Field interface": "Alan arayüzü",
"Title field": "Başlık alanı",
"Date format": "Tarih formatı", "Date format": "Tarih formatı",
"Year/Month/Day": "Yıl/Ay/Gün", "Year/Month/Day": "Yıl/Ay/Gün",
"Year-Month-Day": "Yıl-Ay-Gün", "Year-Month-Day": "Yıl-Ay-Gün",
@ -273,7 +274,6 @@ export default {
"Allow linking to multiple records": "Birden çok kayda bağlanmaya izin ver", "Allow linking to multiple records": "Birden çok kayda bağlanmaya izin ver",
"Allow uploading multiple files": "Birden çok dosya yüklemeye izin ver", "Allow uploading multiple files": "Birden çok dosya yüklemeye izin ver",
"Configure calendar": "Takvimi yapılandır", "Configure calendar": "Takvimi yapılandır",
"Title field": "Başlık alanı",
"Start date field": "Başlangıç tarihi alanı", "Start date field": "Başlangıç tarihi alanı",
"End date field": "Bitiş tarihi alanı", "End date field": "Bitiş tarihi alanı",
"Navigate": "Navigate", "Navigate": "Navigate",
@ -501,4 +501,14 @@ export default {
'Click or drag file to this area to upload': "Dosyayı yüklemek için buraya tıklayın veya sürükleyin", 'Click or drag file to this area to upload': "Dosyayı yüklemek için buraya tıklayın veya sürükleyin",
'Support for a single or bulk upload, file size should not exceed': "Tek veya toplu yükleme destekler, dosya boyutu aşmamalıdır", 'Support for a single or bulk upload, file size should not exceed': "Tek veya toplu yükleme destekler, dosya boyutu aşmamalıdır",
'Default title for each record': "Her kayıt için varsayılan başlık",
'If collection inherits, choose inherited collections as templates': "Koleksiyon miras alırsa, kalıtılan koleksiyonları şablon olarak seçin",
'Select an existing piece of data as the initialization data for the form': 'Formun başlangıç verileri olarak mevcut bir veri parçasını seçin',
'Only the selected fields will be used as the initialization data for the form': 'Yalnızca seçilen alanlar, formun başlangıç verileri olarak kullanılacaktır',
'Template Data': 'Şablon Verisi',
'Data fields': 'Veri alanları',
'Add template': 'Şablon ekle',
'Display data template selector': 'Veri şablonu seçicisini görüntüle',
'Form data templates': 'Form veri şablonları',
} }

View File

@ -227,6 +227,7 @@ export default {
"Optional fields": "可选字段", "Optional fields": "可选字段",
"System fields": "系统字段", "System fields": "系统字段",
"General fields": "普通字段", "General fields": "普通字段",
"Inherited fields": "继承字段",
"Parent collection fields": "父表字段", "Parent collection fields": "父表字段",
"Basic": "基本类型", "Basic": "基本类型",
"Single line text": "单行文本", "Single line text": "单行文本",
@ -743,4 +744,14 @@ export default {
'Click or drag file to this area to upload': '点击或拖拽文件到此区域上传', 'Click or drag file to this area to upload': '点击或拖拽文件到此区域上传',
'Support for a single or bulk upload, file size should not exceed': '支持单个或批量上传,文件大小不能超过', 'Support for a single or bulk upload, file size should not exceed': '支持单个或批量上传,文件大小不能超过',
'Default title for each record': '用作数据的默认标题',
'If collection inherits, choose inherited collections as templates': '当前表有继承关系时,可选择继承链路上的表作为模板来源',
'Select an existing piece of data as the initialization data for the form': '选择一条已有的数据作为表单的初始化数据',
'Only the selected fields will be used as the initialization data for the form': '仅选择的字段才会作为表单的初始化数据',
'Template Data': '模板数据',
'Data fields': '数据字段',
'Add template': '添加模板',
'Display data template selector': '显示数据模板选择框',
'Form data templates': '表单数据模板',
} }

View File

@ -89,7 +89,7 @@ export const Action: ComposedAction = observer((props: any) => {
const values = useRecord(); const values = useRecord();
const designerProps = fieldSchema['x-designer-props']; const designerProps = fieldSchema['x-designer-props'];
const openMode = fieldSchema?.['x-component-props']?.['openMode']; const openMode = fieldSchema?.['x-component-props']?.['openMode'];
const disabled = form.disabled || field.disabled; const disabled = form.disabled || field.disabled || props.disabled;
const openSize = fieldSchema?.['x-component-props']?.['openSize']; const openSize = fieldSchema?.['x-component-props']?.['openSize'];
const linkageRules = fieldSchema?.['x-linkage-rules'] || []; const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
const { designable } = useDesignable(); const { designable } = useDesignable();

View File

@ -2,16 +2,16 @@ import { MenuOutlined } from '@ant-design/icons';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { ArrayField, Field } from '@formily/core'; import { ArrayField, Field } from '@formily/core';
import { import {
observer,
RecursionField, RecursionField,
Schema, Schema,
SchemaExpressionScopeContext,
observer,
useField, useField,
useFieldSchema, useFieldSchema,
SchemaExpressionScopeContext,
} from '@formily/react'; } from '@formily/react';
import { Table, TableColumnProps } from 'antd'; import { Table, TableColumnProps } from 'antd';
import { default as classNames, default as cls } from 'classnames'; import { default as classNames, default as cls } from 'classnames';
import React, { useState, useContext } from 'react'; import React, { useContext, useState } from 'react';
import ReactDragListView from 'react-drag-listview'; import ReactDragListView from 'react-drag-listview';
import { DndContext } from '../..'; import { DndContext } from '../..';
import { RecordIndexProvider, RecordProvider, useRequest, useSchemaInitializer } from '../../../'; import { RecordIndexProvider, RecordProvider, useRequest, useSchemaInitializer } from '../../../';
@ -35,7 +35,7 @@ const useTableColumns = () => {
if (isColumnComponent(s) && useScope(s['x-visible'])) { if (isColumnComponent(s) && useScope(s['x-visible'])) {
return buf.concat([s]); return buf.concat([s]);
} }
return buf return buf;
}, []) }, [])
.map((s: Schema) => { .map((s: Schema) => {
return { return {

View File

@ -1,6 +1,8 @@
import { connect, mapProps, observer } from '@formily/react'; import { connect, mapProps, observer } from '@formily/react';
import { Tree as AntdTree } from 'antd'; import { Tree as AntdTree } from 'antd';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCollectionManager } from '../../collection-manager';
import { AssociationSelect, SchemaComponent } from '../../schema-component'; import { AssociationSelect, SchemaComponent } from '../../schema-component';
import { AsDefaultTemplate } from './components/AsDefaultTemplate'; import { AsDefaultTemplate } from './components/AsDefaultTemplate';
import { ArrayCollapse } from './components/DataTemplateTitle'; import { ArrayCollapse } from './components/DataTemplateTitle';
@ -19,6 +21,9 @@ export const FormDataTemplates = observer((props: any) => {
const { useProps } = props; const { useProps } = props;
const { defaultValues, collectionName } = useProps(); const { defaultValues, collectionName } = useProps();
const { collectionList, getEnableFieldTree } = useCollectionState(collectionName); const { collectionList, getEnableFieldTree } = useCollectionState(collectionName);
const { getCollection } = useCollectionManager();
const collection = getCollection(collectionName);
const { t } = useTranslation();
return ( return (
<SchemaComponent <SchemaComponent
@ -54,7 +59,7 @@ export const FormDataTemplates = observer((props: any) => {
type: 'string', type: 'string',
title: '{{ t("Collection") }}', title: '{{ t("Collection") }}',
required: true, required: true,
description: '当前表有继承关系时,可选择继承链路上的表作为模板来源', description: t('If collection inherits, choose inherited collections as templates'),
default: collectionName, default: collectionName,
'x-display': collectionList.length > 1 ? 'visible' : 'hidden', 'x-display': collectionList.length > 1 ? 'visible' : 'hidden',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
@ -67,7 +72,7 @@ export const FormDataTemplates = observer((props: any) => {
type: 'number', type: 'number',
title: '{{ t("Template Data") }}', title: '{{ t("Template Data") }}',
required: true, required: true,
description: '选择一条已有的数据作为表单的初始化数据', description: t('Select an existing piece of data as the initialization data for the form'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': AssociationSelect, 'x-component': AssociationSelect,
'x-component-props': { 'x-component-props': {
@ -79,14 +84,14 @@ export const FormDataTemplates = observer((props: any) => {
objectValue: false, objectValue: false,
manual: false, manual: false,
mapOptions: (item) => { mapOptions: (item) => {
// TODO: 应该使用 item.title 字段的值作为 label
return { return {
label: item.id, ...item,
[collection.titleField || 'label']: `#${item.id} ${item[collection.titleField] || ''}`,
value: item.id, value: item.id,
}; };
}, },
fieldNames: { fieldNames: {
label: 'label', label: collection.titleField || 'label',
value: 'value', value: 'value',
}, },
}, },
@ -110,7 +115,7 @@ export const FormDataTemplates = observer((props: any) => {
type: 'array', type: 'array',
title: '{{ t("Data fields") }}', title: '{{ t("Data fields") }}',
required: true, required: true,
description: '仅选择的字段才会作为表单的初始化数据', description: t('Only the selected fields will be used as the initialization data for the form'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': Tree, 'x-component': Tree,
'x-component-props': { 'x-component-props': {

View File

@ -164,6 +164,7 @@
"Optional fields": "Optional fields", "Optional fields": "Optional fields",
"System fields": "System fields", "System fields": "System fields",
"General fields": "General fields", "General fields": "General fields",
"Inherited fields": "Inherited fields",
"Parent collection fields": "Parent collection fields", "Parent collection fields": "Parent collection fields",
"Basic": "Basic", "Basic": "Basic",
"Single line text": "Single line text", "Single line text": "Single line text",

View File

@ -149,6 +149,7 @@
"Optional fields": "Campos opcionais", "Optional fields": "Campos opcionais",
"System fields": "Campos do sistema", "System fields": "Campos do sistema",
"General fields": "Campos gerais", "General fields": "Campos gerais",
"Inherited fields": "Campos herdados",
"Parent collection fields": "Campos da coleção pai", "Parent collection fields": "Campos da coleção pai",
"Basic": "Básico", "Basic": "Básico",
"Single line text": "Texto de linha única", "Single line text": "Texto de linha única",

View File

@ -164,6 +164,7 @@
"Optional fields": "可选字段", "Optional fields": "可选字段",
"System fields": "系统字段", "System fields": "系统字段",
"General fields": "普通字段", "General fields": "普通字段",
"Inherited fields": "继承字段",
"Parent collection fields": "父表字段", "Parent collection fields": "父表字段",
"Basic": "基本类型", "Basic": "基本类型",
"Single line text": "单行文本", "Single line text": "单行文本",