feat(filter-blocks): support filter-blocks (#1505)

* refactor: audit log (#1516)

* refactor: audit-log

* refactor: audit-log fix view action

* refactor: audit-log view fix

* refactor(audit-log): collection field fix

* refactor: audit-log view field fix

* refactor(audit-log): support fixedBlock

* refactor(audit-log): i18n fix

* feat(filter-blocks): support form-block

* feat(filter-blocks): add FilterProvider

* feat: filter data blocks

* perf: use useMemo

* refactor: rename

* feat: collect filter params

* refactor: rename

* refactor: remove useless code

* feat: add 'Connect data blocks' option

* feat: support 'x-filter-targets' to save data-blocks

* refactor: extract Form.FilterFormDesigner

* feat: extract FormItem.FilterFormDesigner

* refactor: extract common editing options

* feat: support to set operator

* feat: use operator created by user

* fix: improve loading

* fix: merge prev params

* feat: support reset

* refactor: rename

* chore: left a TODO

* feat: add Table.FilterDesigner

* feat: support filter-table

* refactor: reduce code

* feat: handle click event of table row

* feat: support to connect association collection

* feat: support Collapse

* feat: show empty

* refactor: optimize readability

* fix: keep state as latest

* fix: highlight row on selected

* feat: highlight data block on hover

* fix: avoid misuse

* chore: reduce code

* fix(Table): support to cancel select

* fix(Collapse): merge multiple filter params

* chore: make to pass CI

* feat: merge all filter params

* refactor: remove useless code

* fix: undefined

* fix(Form): fix bug with association fields

* chore: fix typo

* fix: use title

* chore: avoid infinite loops

* test: cancel comments

* fix: make ci normal

* fix: filter down non-association fields

* fix: fix page crash

* fix: use correct operator

* fix: avoid infinte loops

* style: optimize style on hover

* fix: avoid crash

* chore: optimize empty description

* fix: avoid targetKey empty

* refactor: use getTargetKey instead

* fix: filter out unfilterable fields

* refactor: avoid to invoke hook multiple times

* refactor: reduce the judgment conditions of component

* fix: group fields in the right way

* fix: fix error of type

* fix: fix error on no FilterBlockProvider

* fix(Table): fix fexed-block bug

* chore: reduce gap

* fix(Form): use AssociationSelect by default

* fix: remove g2plot blocks

* fix(Form): remove 'Display association fields'

* fix(Table): use radio

* fix(Table): no need Actions

* fix: fix template problem

* fix(Table): keep only 'filter' and 'refresh'

* fix: use collection name as identifier for data blocks

* fix: make sure all fields are editable

* fix(Form): remove custom actions

* fix(Details): display empty component on no data

* feat(Form): support association fields

* refactor: rename

* feat(Form): support for deep-level association fields

* Revert "fix(Table): keep only 'filter' and 'refresh'"

This reverts commit 61a1d101a7d15223cfd3523adb33567fff545568.

* Revert "fix(Table): no need Actions"

This reverts commit 8314629e92fb3b5b7ec1c97904a42c76c43e4d4a.

* Revert "fix(Table): use radio"

This reverts commit c6f009740e1835f9762653a721ff64f52d0994cf.

* feat(Table): highlight row on selected

* feat: support to cacel highlight

* fix: type error

* feat: remove Table from filter list

* fix(Table): highlight rows problem

* refactor: remove usless code

* refactor(Collapse): detach from Table

* fix(Table): highlighting row problem

* fix: translate problem

* fix(Collapse): fix error of useProps

* fix(Table): avoid undefined

* Update Details.tsx

* Update DetailsBlockProvider.tsx

* refactor: rename target.name to target.uid

* fix: add translate

* style: add padding

---------

Co-authored-by: anuoua <anuoua@gmail.com>
Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
被雨水过滤的空气-Rairn 2023-03-20 17:40:16 +08:00 committed by GitHub
parent ef47cac5e6
commit 070c9c21ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 3017 additions and 374 deletions

View File

@ -42,7 +42,7 @@ export class APIClient extends APIClientSDK {
if (redirectTo) {
return (window.location.href = redirectTo);
}
if (error.response.data.type === 'application/json') {
if (error?.response?.data?.type === 'application/json') {
handleErrorMessage(error);
} else {
notification.error({

View File

@ -16,6 +16,7 @@ import {
WithoutTableFieldResource,
} from '../';
import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager';
import { FilterBlockRecord } from '../filter-provider/FilterProvider';
import { useRecordIndex } from '../record-provider';
import { SharedFilterProvider } from './SharedFilterProvider';
@ -34,7 +35,7 @@ interface UseResourceProps {
block?: any;
}
const useAssociation = (props) => {
export const useAssociation = (props) => {
const { association } = props;
const { getCollectionField } = useCollectionManager();
if (typeof association === 'string') {
@ -191,6 +192,7 @@ export const RenderChildrenWithAssociationFilter: React.FC<any> = (props) => {
width: 200px;
flex: 0 0 auto;
`}
style={props.associationFilterStyle}
>
<RecursionField
schema={fieldSchema}
@ -233,7 +235,9 @@ export const BlockProvider = (props) => {
<BlockAssociationContext.Provider value={association}>
<BlockResourceContext.Provider value={resource}>
<BlockRequestProvider {...props}>
<SharedFilterProvider {...props} />
<SharedFilterProvider {...props}>
<FilterBlockRecord {...props}>{props.children}</FilterBlockRecord>
</SharedFilterProvider>
</BlockRequestProvider>
</BlockResourceContext.Provider>
</BlockAssociationContext.Provider>

View File

@ -53,7 +53,9 @@ export const useDetailsBlockProps = () => {
const ctx = useDetailsBlockContext();
useEffect(() => {
if (!ctx.service.loading) {
ctx.form.reset().then(() => {
ctx.form.setValues(ctx.service?.data?.data?.[0] || {});
});
}
}, [ctx.service.loading]);
return {

View File

@ -37,8 +37,6 @@ const InternalFormFieldProvider = (props) => {
return <Spin />;
}
console.log('InternalFormFieldProvider', fieldName);
return (
<RecordProvider record={service?.data?.data}>
<FormFieldContext.Provider
@ -61,7 +59,6 @@ const InternalFormFieldProvider = (props) => {
export const WithoutFormFieldResource = createContext(null);
export const FormFieldProvider = (props) => {
console.log('FormFieldProvider', props);
return (
<WithoutFormFieldResource.Provider value={false}>
<BlockProvider block={'FormField'} {...props}>

View File

@ -3,19 +3,27 @@ import { FormContext, Schema, useField, useFieldSchema } from '@formily/react';
import uniq from 'lodash/uniq';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useCollectionManager } from '../collection-manager';
import { SchemaComponentOptions, useFixedSchema } from '../schema-component';
import { SchemaComponentOptions, useFixedSchema, removeNullCondition } from '../schema-component';
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
import { useFilterBlock } from '../filter-provider/FilterProvider';
import { findFilterTargets } from './hooks';
import { mergeFilter } from './SharedFilterProvider';
export const TableBlockContext = createContext<any>({});
const InternalTableBlockProvider = (props) => {
interface Props {
params?: any;
showIndex?: boolean;
dragSort?: boolean;
rowKey?: string;
childrenColumnName: any;
}
const InternalTableBlockProvider = (props: Props) => {
const { params, showIndex, dragSort, rowKey, childrenColumnName } = props;
const field = useField();
const { resource, service } = useBlockRequestContext();
const [expandFlag, setExpandFlag] = useState(false);
// if (service.loading) {
// return <Spin />;
// }
useFixedSchema();
return (
<TableBlockContext.Provider
@ -91,7 +99,7 @@ export const TableBlockProvider = (props) => {
const fieldSchema = useFieldSchema();
const { getCollection, getCollectionField } = useCollectionManager();
const collection = getCollection(props.collection);
const { treeTable } = fieldSchema?.['x-decorator-props']||{};
const { treeTable } = fieldSchema?.['x-decorator-props'] || {};
if (props.dragSort) {
params['sort'] = ['sort'];
}
@ -104,7 +112,7 @@ export const TableBlockProvider = (props) => {
params['tree'] = true;
}
} else {
const f = collection.fields.find(f => f.treeChildren);
const f = collection.fields.find((f) => f.treeChildren);
if (f) {
childrenColumnName = f.name;
}
@ -135,6 +143,8 @@ export const useTableBlockProps = () => {
const fieldSchema = useFieldSchema();
const ctx = useTableBlockContext();
const globalSort = fieldSchema.parent?.['x-decorator-props']?.['params']?.['sort'];
const { getDataBlocks } = useFilterBlock();
useEffect(() => {
if (!ctx?.service?.loading) {
field.value = ctx?.service?.data?.data;
@ -179,5 +189,53 @@ export const useTableBlockProps = () => {
: globalSort || ctx.service.params?.[0]?.sort;
ctx.service.run({ ...ctx.service.params?.[0], page: current, pageSize, sort });
},
onClickRow(record, setSelectedRow, selectedRow) {
const { targets, uid } = findFilterTargets(fieldSchema);
// 如果是之前创建的区块是没有 x-filter-targets 属性的,所以这里需要判断一下避免报错
if (!targets || !targets.length) return;
const value = [record[ctx.rowKey]];
getDataBlocks().forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const param = block.service.params?.[0] || {};
// 保留原有的 filter
const storedFilter = block.service.params?.[1]?.filters || {};
if (selectedRow.includes(record[ctx.rowKey])) {
delete storedFilter[uid];
} else {
storedFilter[uid] = {
$and: [
{
[target.field || ctx.rowKey]: {
[target.field ? '$in' : '$eq']: value,
},
},
],
};
}
const mergedFilter = mergeFilter([
...Object.values(storedFilter).map((filter) => removeNullCondition(filter)),
block.defaultFilter,
]);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
});
// 更新表格的选中状态
setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [...value]));
},
};
};

View File

@ -4,19 +4,22 @@ import parse from 'json-templates';
import { cloneDeep } from 'lodash';
import get from 'lodash/get';
import omit from 'lodash/omit';
import { useContext } from 'react';
import { ChangeEvent, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { useReactToPrint } from 'react-to-print';
import { useFormBlockContext, useTableBlockContext } from '../..';
import { useAPIClient } from '../../api-client';
import { AssociationFilter, useFormBlockContext, useTableBlockContext } from '../..';
import { useAPIClient, useRequest } from '../../api-client';
import { useCollection } from '../../collection-manager';
import { useFilterBlock } from '../../filter-provider/FilterProvider';
import { transformToFilter } from '../../filter-provider/utils';
import { useRecord } from '../../record-provider';
import { useActionContext, useCompile } from '../../schema-component';
import { removeNullCondition, useActionContext, useCompile } from '../../schema-component';
import { BulkEditFormItemValueType } from '../../schema-initializer/components';
import { useCurrentUserContext } from '../../user';
import { useBlockRequestContext, useFilterByTk } from '../BlockProvider';
import { useDetailsBlockContext } from '../DetailsBlockProvider';
import { mergeFilter } from '../SharedFilterProvider';
import { TableFieldResource } from '../TableFieldProvider';
export const usePickActionProps = () => {
@ -199,6 +202,134 @@ export const useCreateActionProps = () => {
};
};
interface FilterTarget {
targets?: {
/** field uid */
uid: string;
/** associated fields */
field?: string;
}[];
uid?: string;
}
export const findFilterTargets = (fieldSchema): FilterTarget => {
while (fieldSchema) {
if (fieldSchema['x-filter-targets']) {
return {
targets: fieldSchema['x-filter-targets'],
uid: fieldSchema['x-uid'],
};
}
fieldSchema = fieldSchema.parent;
}
return {};
};
export const updateFilterTargets = (fieldSchema, targets: FilterTarget['targets']) => {
while (fieldSchema) {
if (fieldSchema['x-filter-targets']) {
fieldSchema['x-filter-targets'] = targets;
return;
}
fieldSchema = fieldSchema.parent;
}
};
export const useFilterBlockActionProps = () => {
const form = useForm();
const actionField = useField();
const fieldSchema = useFieldSchema();
const { getDataBlocks } = useFilterBlock();
actionField.data = actionField.data || {};
return {
async onClick() {
const { targets = [], uid } = findFilterTargets(fieldSchema);
actionField.data.loading = true;
try {
// 收集 filter 的值
await Promise.all(
getDataBlocks().map(async (block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const param = block.service.params?.[0] || {};
// 保留原有的 filter
const storedFilter = block.service.params?.[1]?.filters || {};
storedFilter[uid] = removeNullCondition(transformToFilter(form.values, fieldSchema));
const mergedFilter = mergeFilter([
...Object.values(storedFilter).map((filter) => removeNullCondition(filter)),
block.defaultFilter,
]);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
}),
);
actionField.data.loading = false;
} catch (error) {
actionField.data.loading = false;
}
},
};
};
export const useResetBlockActionProps = () => {
const form = useForm();
const actionField = useField();
const fieldSchema = useFieldSchema();
const { getDataBlocks } = useFilterBlock();
actionField.data = actionField.data || {};
return {
async onClick() {
const { targets, uid } = findFilterTargets(fieldSchema);
form.reset();
actionField.data.loading = true;
try {
// 收集 filter 的值
await Promise.all(
getDataBlocks().map(async (block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const param = block.service.params?.[0] || {};
// 保留原有的 filter
const storedFilter = block.service.params?.[1]?.filters || {};
delete storedFilter[uid];
const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
}),
);
actionField.data.loading = false;
} catch (error) {
actionField.data.loading = false;
}
},
};
};
export const useCustomizeUpdateActionProps = () => {
const { resource, __parent, service } = useBlockRequestContext();
const filterByTk = useFilterByTk();
@ -663,3 +794,188 @@ export const useDetailsPaginationProps = () => {
},
};
};
export const useAssociationFilterProps = () => {
const collectionField = AssociationFilter.useAssociationField();
const { service, props: blockProps } = useBlockRequestContext();
const fieldSchema = useFieldSchema();
const valueKey = collectionField?.targetKey || 'id';
const labelKey = fieldSchema['x-component-props']?.fieldNames?.label || valueKey;
const collectionFieldName = collectionField.name;
const { data, params, run } = useRequest(
{
resource: collectionField.target,
action: 'list',
params: {
fields: [labelKey, valueKey],
pageSize: 200,
page: 1,
},
},
{
refreshDeps: [labelKey, valueKey],
debounceWait: 300,
},
);
const list = data?.data || [];
const onSelected = (value) => {
const filters = service.params?.[1]?.filters || {};
if (value.length) {
filters[`af.${collectionFieldName}`] = {
[`${collectionFieldName}.${valueKey}.$in`]: value,
};
} else {
delete filters[`af.${collectionFieldName}`];
}
service.run(
{
...service.params?.[0],
pageSize: 200,
page: 1,
filter: mergeFilter([...Object.values(filters), blockProps?.params?.filter]),
},
{ filters },
);
};
const handleSearchInput = (e: ChangeEvent<any>) => {
run({
...params?.[0],
filter: {
[`${labelKey}.$includes`]: e.target.value,
},
});
};
return {
/** 渲染 Collapse 的列表数据 */
list,
onSelected,
handleSearchInput,
params,
run,
};
};
export const useOptionalFieldList = () => {
const { currentFields = [] } = useCollection();
return currentFields.filter((field) => isOptionalField(field) && field.uiSchema.enum);
};
const isOptionalField = (field) => {
const optionalInterfaces = ['select', 'multipleSelect', 'checkbox', 'checkboxGroup', 'chinaRegion'];
return optionalInterfaces.includes(field.interface);
};
export const useAssociationFilterBlockProps = () => {
const collectionField = AssociationFilter.useAssociationField();
const fieldSchema = useFieldSchema();
const optionalFieldList = useOptionalFieldList();
const { getDataBlocks } = useFilterBlock();
const collectionFieldName = collectionField.name;
let list, onSelected, handleSearchInput, params, run, data, valueKey, labelKey, filterKey;
if (isOptionalField(fieldSchema)) {
const field = optionalFieldList.find((field) => field.name === fieldSchema.name);
const operatorMap = {
select: '$in',
multipleSelect: '$anyOf',
checkbox: '$in',
checkboxGroup: '$anyOf',
};
const _list = field?.uiSchema?.enum || [];
valueKey = 'value';
labelKey = 'label';
list = _list;
params = {};
run = () => {};
filterKey = `${field.name}.${operatorMap[field.interface]}`;
handleSearchInput = (e) => {
// TODO: 列表没有刷新,在这个 hook 中使用 useState 会产生 re-render 次数过多的错误
const value = e.target.value;
if (!value) {
list = _list;
return;
}
list = (_list as any[]).filter((item) => item.label.includes(value));
};
} else {
valueKey = collectionField?.targetKey || 'id';
labelKey = fieldSchema['x-component-props']?.fieldNames?.label || valueKey;
({ data, params, run } = useRequest(
{
resource: collectionField.target,
action: 'list',
params: {
fields: [labelKey, valueKey],
pageSize: 200,
page: 1,
},
},
{
refreshDeps: [labelKey, valueKey],
debounceWait: 300,
},
));
filterKey = `${collectionFieldName}.${valueKey}.$in`;
list = data?.data || [];
handleSearchInput = (e: ChangeEvent<any>) => {
run({
...params?.[0],
filter: {
[`${labelKey}.$includes`]: e.target.value,
},
});
};
}
onSelected = (value) => {
const { targets, uid } = findFilterTargets(fieldSchema);
getDataBlocks().forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const key = `${uid}${fieldSchema.name}`;
const param = block.service.params?.[0] || {};
// 保留原有的 filter
const storedFilter = block.service.params?.[1]?.filters || {};
if (value.length) {
storedFilter[key] = {
[filterKey]: value,
};
} else {
delete storedFilter[key];
}
const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
});
};
return {
/** 渲染 Collapse 的列表数据 */
list,
onSelected,
handleSearchInput,
params,
run,
valueKey,
labelKey,
};
};

View File

@ -6,6 +6,8 @@ import { CollectionContext } from '../context';
import { CollectionFieldOptions } from '../types';
import { useCollectionManager } from './useCollectionManager';
export type Collection = ReturnType<typeof useCollection>;
export const useCollection = () => {
const collection = useContext(CollectionContext);
const api = useAPIClient();

View File

@ -0,0 +1,121 @@
import { useField, useFieldSchema } from '@formily/react';
import React, { createContext, useEffect, useRef } from 'react';
import { useBlockRequestContext } from '../block-provider';
import { SharedFilter } from '../block-provider/SharedFilterProvider';
import { CollectionFieldOptions, useCollection } from '../collection-manager';
import { useAssociatedFields } from './utils';
type Collection = ReturnType<typeof useCollection>;
export interface DataBlock {
/** 唯一标识符schema 中的 name 值 */
uid: string;
/** 用户自行设置的区块名称 */
title?: string;
/** 与当前区块相关的数据表信息 */
collection: Collection;
/** 根据提供的参数执行该方法即可刷新数据区块的数据 */
doFilter: (params: any, params2?: any) => Promise<void>;
/** 数据区块表中所有的关系字段 */
associatedFields?: CollectionFieldOptions[];
/** 通过右上角菜单设置的过滤条件 */
defaultFilter?: SharedFilter;
service?: any;
/** 区块所对应的 DOM 容器 */
dom: HTMLElement;
}
interface FilterContextValue {
dataBlocks: DataBlock[];
setDataBlocks: React.Dispatch<React.SetStateAction<DataBlock[]>>;
}
const FilterContext = createContext<FilterContextValue>(null);
/**
* 使
* @param props
* @returns
*/
export const FilterBlockProvider: React.FC = ({ children }) => {
const [dataBlocks, setDataBlocks] = React.useState<DataBlock[]>([]);
return <FilterContext.Provider value={{ dataBlocks, setDataBlocks }}>{children}</FilterContext.Provider>;
};
export const FilterBlockRecord = ({
children,
params,
}: {
children: React.ReactNode;
params?: { filter: SharedFilter };
}) => {
const collection = useCollection();
const { recordDataBlocks, removeDataBlock } = useFilterBlock();
const { service } = useBlockRequestContext();
const field = useField();
const fieldSchema = useFieldSchema();
const associatedFields = useAssociatedFields();
const container = useRef(null);
const shouldApplyFilter = field.decoratorType !== 'FormBlockProvider' && field.decoratorProps.blockType !== 'filter';
const addBlockToDataBlocks = () => {
recordDataBlocks({
uid: fieldSchema['x-uid'],
title: field.componentProps.title,
doFilter: service.runAsync,
collection,
associatedFields,
defaultFilter: params?.filter || {},
service,
dom: container.current,
});
};
useEffect(() => {
if (shouldApplyFilter) addBlockToDataBlocks();
}, [params?.filter, service]);
useEffect(() => {
return () => {
removeDataBlock(field.props.name as string);
};
}, []);
return <div ref={container}>{children}</div>;
};
/**
*
* @returns
*/
export const useFilterBlock = () => {
const ctx = React.useContext(FilterContext);
// 有可能存在页面没有提供 FilterBlockProvider 的情况,比如内部使用的数据表管理页面
if (!ctx) {
return {
recordDataBlocks: () => {},
getDataBlocks: () => [] as DataBlock[],
removeDataBlock: () => {},
};
}
const { dataBlocks, setDataBlocks } = ctx;
const recordDataBlocks = (block: DataBlock) => {
const existingBlock = dataBlocks.find((item) => item.uid === block.uid);
if (existingBlock) {
// 这里的值有可能会变化,所以需要更新
existingBlock.service = block.service;
existingBlock.defaultFilter = block.defaultFilter;
return;
}
setDataBlocks((prev) => [...prev, block]);
};
const getDataBlocks = () => dataBlocks;
const removeDataBlock = (name: string) => {
setDataBlocks((prev) => prev.filter((item) => item.uid !== name));
};
return { recordDataBlocks, getDataBlocks, removeDataBlock };
};

View File

@ -0,0 +1,54 @@
import { render } from '@testing-library/react';
import React from 'react';
import { FilterBlockProvider, useFilterBlock } from '../FilterProvider';
describe('useFilter', () => {
test('should get a empty array', () => {
let getDataBlocks = null;
const Comp = () => {
({ getDataBlocks } = useFilterBlock());
return null;
};
const App = () => {
return (
<FilterBlockProvider>
<Comp />
</FilterBlockProvider>
);
};
render(<App />);
expect(getDataBlocks()).toEqual([]);
});
test('should not repeat', () => {
let getDataBlocks = null,
recordDataBlocks = null;
const Comp = () => {
({ getDataBlocks, recordDataBlocks } = useFilterBlock());
return null;
};
const App = () => {
return (
<FilterBlockProvider>
<Comp />
</FilterBlockProvider>
);
};
render(<App />);
recordDataBlocks({
name: 'test',
collection: {},
doFilter: () => {},
});
expect(getDataBlocks().length).toBe(1);
// avoid repeat
recordDataBlocks({
name: 'test',
collection: {},
doFilter: () => {},
});
expect(getDataBlocks().length).toBe(1);
});
});

View File

@ -0,0 +1,89 @@
import { Schema, useFieldSchema } from '@formily/react';
import { isPlainObject, isEmpty } from '@nocobase/utils/client';
import { Collection, FieldOptions, useCollection } from '../collection-manager';
import { findFilterOperators } from '../schema-component/antd/form-item/SchemaSettingOptions';
import { useFilterBlock } from './FilterProvider';
export enum FilterBlockType {
FORM,
TABLE,
TREE,
COLLAPSE,
}
/**
*
* @param filterBlockType
* @returns
*/
export const useSupportedBlocks = (filterBlockType: FilterBlockType) => {
const { getDataBlocks } = useFilterBlock();
const fieldSchema = useFieldSchema();
const collection = useCollection();
// Form 和 Collapse 仅支持同表的数据区块
if (filterBlockType === FilterBlockType.FORM || filterBlockType === FilterBlockType.COLLAPSE) {
return getDataBlocks().filter((block) => {
return isSameCollection(block.collection, collection);
});
}
// Table 和 Tree 支持同表或者关系表的数据区块
if (filterBlockType === FilterBlockType.TABLE || filterBlockType === FilterBlockType.TREE) {
return getDataBlocks().filter((block) => {
return (
fieldSchema['x-uid'] !== block.uid &&
(isSameCollection(block.collection, collection) ||
block.associatedFields.some((field) => field.target === collection.name))
);
});
}
};
/**
* Recursively flatten an object and generate a query object.
* @param result - The resulting query object.
* @param obj - The object to be flattened and queried.
* @param key - The current key in the object tree.
* @param operators - The operators to use for each field.
* @returns The resulting query object.
*/
const flattenAndQueryObject = (result: Record<string, any>, obj: any, key: string, operators: Record<string, any>) => {
if (!obj) return result;
if (!isPlainObject(obj)) {
result[key] = {
[operators[key] || '$eq']: obj,
};
} else {
Object.keys(obj).forEach((k) => {
flattenAndQueryObject(result, obj[k], `${key}.${k}`, operators);
});
}
return result;
};
export const transformToFilter = (values: Record<string, any>, fieldSchema: Schema) => {
const { operators } = findFilterOperators(fieldSchema);
return {
$and: Object.keys(values)
.map((key) => flattenAndQueryObject({}, values[key], key, operators))
.filter((item) => !isEmpty(item)),
};
};
export const useAssociatedFields = () => {
const { fields } = useCollection();
return fields.filter((field) => isAssocField(field)) || [];
};
export const isAssocField = (field?: FieldOptions) => {
return ['o2o', 'oho', 'obo', 'm2o', 'createdBy', 'updatedBy', 'o2m', 'm2m', 'linkTo'].includes(field?.interface);
};
export const isSameCollection = (c1: Collection, c2: Collection) => {
return c1.name === c2.name;
};

View File

@ -15,6 +15,7 @@ export default {
"Time": "Time",
"Event": "Event",
"None": "None",
"Unconnected": "Unconnected",
"System settings": "System settings",
"System title": "System title",
"Logo": "Logo",
@ -73,8 +74,10 @@ export default {
"Close": "Close",
"Set the data scope": "Set the data scope",
"Data blocks": "Data blocks",
"Filter blocks": "Filter blocks",
"Table": "Table",
"Form": "Form",
"Collapse": "Collapse",
"Select data source": "Select data source",
"Calendar": "Calendar",
"Delete events": "Delete events",
@ -127,6 +130,7 @@ export default {
"Are you sure you want to delete it?": "Are you sure you want to delete it?",
"This is a demo text, **supports Markdown syntax**.": "This is a demo text, **supports Markdown syntax**.",
"Filter": "Filter",
"Connect data blocks": "Connect data blocks",
"Action type": "Action type",
"Actions": "Actions",
"Insert": "Insert",
@ -166,6 +170,7 @@ export default {
"Association fields filter": "Association fields filter",
"PK & FK fields": "PK & FK fields",
"Association fields": "Association fields",
"Optional fields": "Optional fields",
"System fields": "System fields",
"General fields": "General fields",
"Parent collection fields": "Parent collection fields",
@ -244,6 +249,7 @@ export default {
"Edit block title": "Edit block title",
"Block title": "Block title",
"Pattern": "Pattern",
"Operator": "Operator",
"Editable": "Editable",
"Readonly": "Readonly",
"Easy-reading": "Easy-reading",
@ -414,6 +420,7 @@ export default {
"Convert reference to duplicate": "Convert reference to duplicate",
"Template name": "Template name",
"Block type": "Block type",
"No blocks to connect": "No blocks to connect",
"Action column": "Action column",
"Records per page": "Records per page",
"(Fields only)": "(Fields only)",

View File

@ -15,6 +15,7 @@ export default {
"Time": "時間",
"Event": "イベント",
"None": "なし",
"Unconnected": "未接続",
"System settings": "システム設定",
"System title": "システム名",
"Logo": "ロゴ",
@ -77,8 +78,10 @@ export default {
"Close": "閉じる",
"Set the data scope": "データ範囲の設定",
"Data blocks": "データブロック",
"Filter blocks": "フィルターブロック",
"Table": "テーブル",
"Form": "フォーム",
"Collapse": "折りたたみ",
"Select data source": "データソースを選択",
"Calendar": "カレンダー",
"Kanban": "かんばん",
@ -120,6 +123,7 @@ export default {
"Are you sure you want to delete it?": "本当に削除しますか?",
"This is a demo text, **supports Markdown syntax**.": "これはデモテキストです。 **マークダウン構文をサポートしています。**",
"Filter": "フィルター",
"Connect data blocks": "データブロックを連結",
"Action type": "操作タイプ",
"Actions": "操作",
"Insert": "作成",
@ -346,6 +350,7 @@ export default {
"Convert reference to duplicate": "参照を複製に変換",
"Template name": "テンプレート名",
"Block type": "ブロックタイプ",
"No blocks to connect": "接続するブロックがありません",
"Action column": "操作カラム",
"Records per page": "ページごとのレコード数",
"(Fields only)": "(フィールドのみ)",
@ -531,6 +536,7 @@ export default {
"Enable SMS authentication": "SMS認証を有効にする",
"Display association fields": "関連付けられたコレクションのフィールドを表示",
"Set default value": "デフォルト値を設定",
"Optional fields": "オプションフィールド",
"Editable": "編集可能",
"Readonly": "読み取り専用(編集不可)",
"Easy-reading": "読取り専用(読取りモード)",

View File

@ -15,6 +15,7 @@ export default {
"Time": "Время",
"Event": "Событие",
"None": "Ничего",
"Unconnected": "Не подключен",
"System settings": "Системные настройки",
"System title": "Системный заголовок",
"Logo": "Логотип",
@ -49,8 +50,10 @@ export default {
"Close": "Закрыть",
"Set the data scope": "Установить область данных",
"Data blocks": "Блоки данных",
"Filter blocks": "Просеивающие блоки",
"Table": "Таблица",
"Form": "Форма",
"Collapse": "Свернуть",
"Select data source": "Выбрать источник данных",
"Calendar": "Календарь",
"Kanban": "Канбан",
@ -92,6 +95,7 @@ export default {
"Are you sure you want to delete it?": "Вы уверены, что хотите удалить это?",
"This is a demo text, **supports Markdown syntax**.": "Это демо текст, **поддерживает синтаксис Markdown**.",
"Filter": "Фильтр",
"Connect data blocks": "Соединить блоки данных",
"Action type": "Тип действия",
"Actions": "Действия",
"Insert": "Вставить",
@ -307,6 +311,7 @@ export default {
"Convert reference to duplicate": "Преобразовать ссылку в дубликат",
"Template name": "Имя Шаблона",
"Block type": "Тип Блока",
"No blocks to connect": "Нет Блоков для подключения",
"Action column": "Колонка действий",
"Records per page": "Записей на страницу",
"(Fields only)": "(Только поля)",

View File

@ -15,6 +15,7 @@ export default {
"Time": "Saat",
"Event": "Olay",
"None": "Boş",
"Unconnected": "Bağlantı yok",
"System settings": "Sistem ayarları",
"System title": "Sistem başlığı",
"Logo": "Logo",
@ -49,8 +50,10 @@ export default {
"Close": "Kapat",
"Set the data scope": "Veri kapsamını ayarla",
"Data blocks": "Veri Blokları",
"Filter blocks": "Filtre blokları",
"Table": "Tablo",
"Form": "Form",
"Collapse": "Daralt",
"Select data source": "Veri kaynağını seç",
"Calendar": "Takvim",
"Kanban": "Kanban",
@ -92,6 +95,7 @@ export default {
"Are you sure you want to delete it?": "Silmek istediğinizden emin misiniz?",
"This is a demo text, **supports Markdown syntax**.": "Bu bir örnek yazıdır, **işaretleme yazısı destekleniyor**.",
"Filter": "Filtre",
"Connect data blocks": "Veri bloklarını bağla",
"Action type": "İşlem Türü",
"Actions": "İşlemler",
"Insert": "Ekle",
@ -306,6 +310,7 @@ export default {
"Convert reference to duplicate": "Referansı kopyaya dönüştür",
"Template name": "Şablon adı",
"Block type": "Blok türü",
"No blocks to connect": "Bağlanacak blok yok",
"Action column": "İşlem sütunu",
"Records per page": "Sayfa başına kayıt",
"(Fields only)": "(Sadece alanlar)",
@ -461,6 +466,7 @@ export default {
"Use the same time zone (GMT) for all users": "Tüm kullanıcılar için aynı saat dilimini (GMT) kullanın",
"Block title": "Blok başlığı",
"Edit block title": "Blok başlığını düzenle",
"operater": "operatör",
"Province/city/area name": "Semt/şehir/bölge adı",
"Field component": "Alan bileşeni",
"Subtable": "Alttablo",

View File

@ -15,6 +15,7 @@ export default {
"Time": "时间",
"Event": "事件",
"None": "无",
"Unconnected": "未连接",
"System settings": "系统设置",
"System title": "系统名称",
"Logo": "Logo",
@ -83,8 +84,10 @@ export default {
"Close": "关闭",
"Set the data scope": "设置数据范围",
"Data blocks": "数据区块",
"Filter blocks": "筛选区块",
"Table": "表格",
"Form": "表单",
"Collapse": "折叠面板",
"Select data source": "选择数据源",
"Calendar": "日历",
'Delete events': '删除日程',
@ -133,6 +136,7 @@ export default {
"This is a demo text, **supports Markdown syntax**.": "这是一段演示文本,**支持 Markdown 语法**。",
"Filter": "筛选",
"Connect data blocks": "连接数据区块",
"Action type": "操作类型",
"Actions": "操作",
"Insert": "新增",
@ -175,6 +179,7 @@ export default {
"Association fields filter": "关系筛选",
"PK & FK fields": "主外键字段",
"Association fields": "关系字段",
"Optional fields": "可选字段",
"System fields": "系统字段",
"General fields": "普通字段",
"Parent collection fields": "父表字段",
@ -260,6 +265,7 @@ export default {
"Edit block title": "编辑区块标题",
"Block title": "区块标题",
"Pattern": "模式",
"Operator": "运算符",
"Editable": "可编辑",
"Readonly": "只读(禁止编辑)",
"Easy-reading": "只读(阅读模式)",
@ -446,6 +452,7 @@ export default {
"Convert reference to duplicate": "模板引用转为复制",
"Template name": "模板名称",
"Block type": "区块类型",
"No blocks to connect": "没有可连接的区块",
"Action column": "操作列",
"Records per page": "每页显示数量",
"(Fields only)": "(仅字段)",

View File

@ -0,0 +1,30 @@
import { useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCollection } from '../../../collection-manager';
import { FilterBlockType } from '../../../filter-provider/utils';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
export const AssociationFilterBlockDesigner = () => {
const { name, title } = useCollection();
const template = useSchemaTemplate();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
return (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem />
<SchemaSettings.Template componentName={'FilterCollapse'} collectionName={name} resourceName={defaultResource} />
<SchemaSettings.ConnectDataBlocks type={FilterBlockType.COLLAPSE} emptyDescription={t('No blocks to connect')} />
<SchemaSettings.Divider />
<SchemaSettings.Remove
removeParentsIfNoChildren
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -0,0 +1,90 @@
import { css } from '@emotion/css';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useOptionalFieldList } from '../../../block-provider/hooks';
import { useAssociatedFields } from '../../../filter-provider/utils';
import { SchemaInitializer, SchemaInitializerItemOptions } from '../../../schema-initializer';
export const AssociationFilterFilterBlockInitializer = () => {
const { t } = useTranslation();
const associatedFields = useAssociatedFields();
const optionalList = useOptionalFieldList();
const useProps = '{{useAssociationFilterBlockProps}}';
const children: SchemaInitializerItemOptions[] = associatedFields.map((field) => ({
type: 'item',
key: field.key,
title: field.uiSchema?.title,
component: 'AssociationFilterDesignerDisplayField',
schema: {
name: field.name,
title: field.uiSchema?.title,
type: 'void',
'x-designer': 'AssociationFilter.Item.Designer',
'x-component': 'AssociationFilter.Item',
'x-component-props': {
fieldNames: {
label: field.targetKey || 'id',
},
useProps,
},
properties: {},
},
}));
const optionalChildren: SchemaInitializerItemOptions[] = optionalList.map((field) => ({
type: 'item',
key: field.key,
title: field.uiSchema.title,
component: 'AssociationFilterDesignerDisplayField',
schema: {
name: field.name,
title: field.uiSchema.title,
interface: field.interface,
type: 'void',
'x-designer': 'AssociationFilter.Item.Designer',
'x-component': 'AssociationFilter.Item',
'x-component-props': {
fieldNames: {
label: field.name,
},
useProps,
},
properties: {},
},
}));
const associatedFieldGroup: SchemaInitializerItemOptions = {
type: 'itemGroup',
title: t('Association fields'),
children,
};
// 可选项字段
const optionalFieldGroup: SchemaInitializerItemOptions = {
type: 'itemGroup',
title: t('Optional fields'),
children: optionalChildren,
};
const dividerItem: SchemaInitializerItemOptions = {
type: 'divider',
};
const deleteItem: SchemaInitializerItemOptions = {
type: 'item',
title: t('Delete'),
component: 'AssociationFilterDesignerDelete',
};
const items = [associatedFieldGroup, optionalFieldGroup];
return (
<SchemaInitializer.Button
className={css`
margin-top: 16px;
`}
icon={'SettingOutlined'}
title={t('Configure fields')}
items={items}
/>
);
};

View File

@ -1,18 +1,14 @@
import { css } from '@emotion/css';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCollection } from '../../../collection-manager';
import { useAssociatedFields } from '../../../filter-provider/utils';
import { SchemaInitializer, SchemaInitializerItemOptions } from '../../../schema-initializer';
export const AssociationFilterInitializer = () => {
const { t } = useTranslation();
const { fields } = useCollection();
const associatedFields = fields.filter((field) =>
['o2o', 'oho', 'obo', 'm2o', 'createdBy', 'updatedBy', 'o2m', 'm2m', 'linkTo'].includes(field.interface),
);
const items: SchemaInitializerItemOptions[] = associatedFields.map((field) => ({
const associatedFields = useAssociatedFields();
const useProps = '{{useAssociationFilterProps}}';
const children: SchemaInitializerItemOptions[] = associatedFields.map((field) => ({
type: 'item',
key: field.key,
title: field.uiSchema?.title,
@ -27,6 +23,7 @@ export const AssociationFilterInitializer = () => {
fieldNames: {
label: field.targetKey || 'id',
},
useProps,
},
properties: {},
},
@ -35,7 +32,7 @@ export const AssociationFilterInitializer = () => {
const associatedFieldGroup: SchemaInitializerItemOptions = {
type: 'itemGroup',
title: t('Association fields'),
children: items,
children,
};
const dividerItem: SchemaInitializerItemOptions = {
@ -48,6 +45,8 @@ export const AssociationFilterInitializer = () => {
component: 'AssociationFilterDesignerDelete',
};
const items = [associatedFieldGroup, dividerItem, deleteItem];
return (
<SchemaInitializer.Button
className={css`
@ -55,7 +54,7 @@ export const AssociationFilterInitializer = () => {
`}
icon={'SettingOutlined'}
title={t('Configure fields')}
items={[associatedFieldGroup, dividerItem, deleteItem]}
items={items}
/>
);
};

View File

@ -4,9 +4,7 @@ import { useFieldSchema } from '@formily/react';
import { Col, Collapse, Input, Row, Tree } from 'antd';
import cls from 'classnames';
import React, { ChangeEvent, MouseEvent, useState } from 'react';
import { useRequest } from '../../../api-client';
import { useBlockRequestContext } from '../../../block-provider';
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { useAssociationFilterProps } from '../../../block-provider/hooks';
import { SortableItem } from '../../common';
import { useCompile, useDesigner } from '../../hooks';
import { AssociationFilter } from './AssociationFilter';
@ -16,49 +14,40 @@ const { Panel } = Collapse;
export const AssociationFilterItem = (props) => {
const collectionField = AssociationFilter.useAssociationField();
if (!collectionField) {
return null;
}
// 把一些可定制的状态通过 hook 提取出去了,为了兼容之前添加的 Table 区块,这里加了个默认值
const { useProps = useAssociationFilterProps } = props;
const fieldSchema = useFieldSchema();
const Designer = useDesigner();
const compile = useCompile();
const { service, props: blockProps } = useBlockRequestContext();
const {
list,
onSelected,
handleSearchInput: _handleSearchInput,
params,
run,
valueKey: _valueKey,
labelKey: _labelKey,
} = useProps();
const [searchVisible, setSearchVisible] = useState(false);
const collectionFieldName = collectionField.name;
const valueKey = collectionField?.targetKey || 'id';
const labelKey = fieldSchema['x-component-props']?.fieldNames?.label || valueKey;
const valueKey = _valueKey || collectionField?.targetKey || 'id';
const labelKey = _labelKey || fieldSchema['x-component-props']?.fieldNames?.label || valueKey;
const fieldNames = {
title: labelKey || valueKey,
key: valueKey,
};
const { data, params, loading, run } = useRequest(
{
resource: collectionField.target,
action: 'list',
params: {
fields: [labelKey, valueKey],
pageSize: 200,
page: 1,
},
},
{
refreshDeps: [labelKey, valueKey],
debounceWait: 300,
},
);
const treeData = data?.data || [];
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);
if (!collectionField) {
return null;
}
const onExpand = (expandedKeysValue: React.Key[]) => {
setExpandedKeys(expandedKeysValue);
setAutoExpandParent(false);
@ -66,26 +55,7 @@ export const AssociationFilterItem = (props) => {
const onSelect = (selectedKeysValue: React.Key[]) => {
setSelectedKeys(selectedKeysValue);
const filters = service.params?.[1]?.filters || {};
if (selectedKeysValue.length) {
filters[`af.${collectionFieldName}`] = {
[`${collectionFieldName}.${valueKey}.$in`]: selectedKeysValue,
};
} else {
delete filters[`af.${collectionFieldName}`];
}
service.run(
{
...service.params?.[0],
pageSize: 200,
page: 1,
filter: mergeFilter([...Object.values(filters), blockProps?.params?.filter]),
},
{ filters },
);
onSelected(selectedKeysValue);
};
const handleSearchToggle = (e: MouseEvent) => {
@ -105,12 +75,7 @@ export const AssociationFilterItem = (props) => {
};
const handleSearchInput = (e: ChangeEvent<any>) => {
run({
...params?.[0],
filter: {
[`${labelKey}.$includes`]: e.target.value,
},
});
_handleSearchInput(e);
};
const title = fieldSchema.title ?? collectionField.uiSchema?.title;
@ -258,7 +223,7 @@ export const AssociationFilterItem = (props) => {
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
treeData={treeData}
treeData={list}
onSelect={onSelect}
fieldNames={fieldNames}
titleRender={(node) => compile(node[labelKey])}

View File

@ -6,15 +6,18 @@ import { useCollection } from '../../../collection-manager';
import { useSchemaInitializer } from '../../../schema-initializer';
import { DndContext, SortableItem } from '../../common';
import { useDesigner } from '../../hooks';
import { AssociationFilterBlockDesigner } from './AssociationFilter.BlockDesigner';
import { AssociationFilterFilterBlockInitializer } from './AssociationFilter.FilterBlockInitializer';
import { AssociationFilterInitializer } from './AssociationFilter.Initializer';
import { AssociationFilterItem } from './AssociationFilter.Item';
import { AssociationFilterItemDesigner } from './AssociationFilter.Item.Designer';
import { AssociationFilterProvider } from './AssociationFilterProvider';
export const AssociationFilter = (props) => {
const Designer = useDesigner();
const filedSchema = useFieldSchema();
const { exists, render } = useSchemaInitializer(filedSchema['x-initializer']);
const { render } = useSchemaInitializer(filedSchema['x-initializer']);
return (
<DndContext>
@ -75,11 +78,14 @@ export const AssociationFilter = (props) => {
);
};
AssociationFilter.Provider = AssociationFilterProvider;
AssociationFilter.Initializer = AssociationFilterInitializer;
AssociationFilter.FilterBlockInitializer = AssociationFilterFilterBlockInitializer;
AssociationFilter.Item = AssociationFilterItem as typeof AssociationFilterItem & {
Designer: typeof AssociationFilterItemDesigner;
};
AssociationFilter.Item.Designer = AssociationFilterItemDesigner;
AssociationFilter.BlockDesigner = AssociationFilterBlockDesigner;
AssociationFilter.useAssociationField = () => {
const fieldSchema = useFieldSchema();

View File

@ -0,0 +1,2 @@
// TODO: 因他们之间功能相同,所以先直接复用,后续有需要再拆分
export { TableBlockProvider as AssociationFilterProvider } from '../../../block-provider';

View File

@ -0,0 +1,5 @@
import { CollectionFieldOptions } from '../../../collection-manager';
export const getTargetKey = (field?: CollectionFieldOptions) => {
return field?.targetKey || 'id';
};

View File

@ -84,6 +84,7 @@ const InternalAssociationSelect = connect(
interface AssociationSelectInterface {
(props: any): React.ReactElement;
Designer: React.FC;
FilterDesigner: React.FC;
}
export const AssociationSelect = InternalAssociationSelect as unknown as AssociationSelectInterface;
@ -680,4 +681,481 @@ AssociationSelect.Designer = () => {
);
};
/**
*
* @returns
*/
AssociationSelect.FilterDesigner = () => {
const { getCollectionFields, getInterface, getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const { form } = useFormBlockContext();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const tk = useFilterByTk();
const {} = useCollection();
const { dn, refresh, insertAdjacent } = useDesignable();
const compile = useCompile();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const fieldComponentOptions = useFieldComponentOptions();
const isSubFormAssocitionField = field.address.segments.includes('__form_grid');
const interfaceConfig = getInterface(collectionField?.interface);
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
const originalTitle = collectionField?.uiSchema?.title;
const targetFields = collectionField?.target ? getCollectionFields(collectionField.target) : [];
const initialValue = {
title: field.title === originalTitle ? undefined : field.title,
};
const sortFields = useSortFields(collectionField?.target);
const defaultSort = field.componentProps?.service?.params?.sort || [];
const defaultFilter = field.componentProps?.service?.params?.filter || {};
const dataSource = useCollectionFilterOptions(collectionField?.target);
const sort = defaultSort?.map((item: string) => {
return item.startsWith('-')
? {
field: item.substring(1),
direction: 'desc',
}
: {
field: item,
direction: 'asc',
};
});
if (!field.readPretty) {
initialValue['required'] = field.required;
}
const options = targetFields
.filter((field) => !field?.target && field.type !== 'boolean')
.map((field) => ({
value: field?.name,
label: compile(field?.uiSchema?.title) || field?.name,
}));
let readOnlyMode = 'editable';
if (fieldSchema['x-disabled'] === true) {
readOnlyMode = 'readonly';
}
if (fieldSchema['x-read-pretty'] === true) {
readOnlyMode = 'read-pretty';
}
return (
<GeneralSchemaDesigner>
{collectionField && (
<SchemaSettings.ModalItem
key="edit-field-title"
title={t('Edit field title')}
schema={
{
type: 'object',
title: t('Edit field title'),
properties: {
title: {
title: t('Field title'),
default: field?.title,
description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ title }) => {
if (title) {
field.title = title;
fieldSchema.title = title;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
title: fieldSchema.title,
},
});
}
dn.refresh();
}}
/>
)}
{!field.readPretty && (
<SchemaSettings.ModalItem
key="edit-description"
title={t('Edit description')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
description: {
// title: t('Description'),
default: field?.description,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ description }) => {
field.description = description;
fieldSchema.description = description;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
description: fieldSchema.description,
},
});
dn.refresh();
}}
/>
)}
{field.readPretty && (
<SchemaSettings.ModalItem
key="edit-tooltip"
title={t('Edit tooltip')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
tooltip: {
default: fieldSchema?.['x-decorator-props']?.tooltip,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ tooltip }) => {
field.decoratorProps.tooltip = tooltip;
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
dn.refresh();
}}
/>
)}
{form && !form?.readPretty && validateSchema && (
<SchemaSettings.ModalItem
title={t('Set validation rules')}
components={{ ArrayCollapse, FormLayout }}
schema={
{
type: 'object',
title: t('Set validation rules'),
properties: {
rules: {
type: 'array',
default: fieldSchema?.['x-validator'],
'x-component': 'ArrayCollapse',
'x-decorator': 'FormItem',
'x-component-props': {
accordion: true,
},
maxItems: 3,
items: {
type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel',
'x-component-props': {
header: '{{ t("Validation rule") }}',
},
properties: {
index: {
type: 'void',
'x-component': 'ArrayCollapse.Index',
},
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
labelStyle: {
marginTop: '6px',
},
labelCol: 8,
wrapperCol: 16,
},
properties: {
...validateSchema,
message: {
type: 'string',
title: '{{ t("Error message") }}',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {
autoSize: {
minRows: 2,
maxRows: 2,
},
},
},
},
},
remove: {
type: 'void',
'x-component': 'ArrayCollapse.Remove',
},
moveUp: {
type: 'void',
'x-component': 'ArrayCollapse.MoveUp',
},
moveDown: {
type: 'void',
'x-component': 'ArrayCollapse.MoveDown',
},
},
},
properties: {
add: {
type: 'void',
title: '{{ t("Add validation rule") }}',
'x-component': 'ArrayCollapse.Addition',
'x-reactions': {
dependencies: ['rules'],
fulfill: {
state: {
disabled: '{{$deps[0].length >= 3}}',
},
},
},
},
},
},
},
} as ISchema
}
onSubmit={(v) => {
const rules = [];
for (const rule of v.rules) {
rules.push(_.pickBy(rule, _.identity));
}
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
// return;
// if (['number'].includes(collectionField?.interface) && collectionField?.uiSchema?.['x-component-props']?.['stringMode'] === true) {
// rules['numberStringMode'] = true;
// }
if (['percent'].includes(collectionField?.interface)) {
for (const rule of rules) {
if (!!rule.maxValue || !!rule.minValue) {
rule['percentMode'] = true;
}
if (rule.percentFormat) {
rule['percentFormats'] = true;
}
}
}
const concatValidator = _.concat([], collectionField?.uiSchema?.['x-validator'] || [], rules);
field.validator = concatValidator;
fieldSchema['x-validator'] = rules;
schema['x-validator'] = rules;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
{form && !form?.readPretty && collectionField?.uiSchema?.type && (
<SchemaSettings.ModalItem
title={t('Set default value')}
components={{ ArrayCollapse, FormLayout }}
schema={
{
type: 'object',
title: t('Set default value'),
properties: {
default: {
...collectionField.uiSchema,
name: 'default',
title: t('Default value'),
'x-decorator': 'FormItem',
default: fieldSchema.default || collectionField.defaultValue,
},
},
} as ISchema
}
onSubmit={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
if (field.value !== v.default) {
field.value = v.default;
}
fieldSchema.default = v.default;
schema.default = v.default;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
<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 }) => {
_.set(field.componentProps, 'service.params.filter', filter);
fieldSchema['x-component-props'] = field.componentProps;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': field.componentProps,
},
});
}}
/>
<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;
});
_.set(field.componentProps, 'service.params.sort', sortArr);
fieldSchema['x-component-props'] = field.componentProps;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': field.componentProps,
},
});
}}
/>
{collectionField?.target && ['CollectionField', 'AssociationSelect'].includes(fieldSchema['x-component']) && (
<SchemaSettings.SelectItem
key="title-field"
title={t('Title field')}
options={options}
value={field?.componentProps?.fieldNames?.label}
onChange={(label) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
const fieldNames = {
...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
...field.componentProps.fieldNames,
label,
};
field.componentProps.fieldNames = fieldNames;
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
schema['x-component-props'] = fieldSchema['x-component-props'];
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
)}
{collectionField && <SchemaSettings.Divider />}
<SchemaSettings.Remove
key="remove"
removeParentsIfNoChildren
confirm={{
title: t('Delete field'),
}}
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};
export default AssociationSelect;

View File

@ -9,6 +9,7 @@ export const CardItem: React.FC = (props) => {
const template = useSchemaTemplate();
const fieldSchema = useFieldSchema();
const templateKey = fieldSchema['x-template-key'];
return templateKey && !template ? null : (
<BlockItem className={'noco-card-item'}>
<Card style={{ marginBottom: 24 }} bordered={false} {...restProps}>

View File

@ -0,0 +1,3 @@
import { FormV2 } from '../form-v2';
export const Details = FormV2;

View File

@ -0,0 +1 @@
export * from './Details';

View File

@ -92,7 +92,7 @@ export const useFilterActionProps = () => {
const { name } = useCollection();
const options = useFilterOptions(name);
const { service, props } = useBlockRequestContext();
return useFilterFieldProps({ options, service, params: props.params });
return useFilterFieldProps({ options, service, params: props?.params });
};
export const useFilterFieldProps = ({ options, service, params }) => {

View File

@ -0,0 +1,21 @@
import { useFieldSchema } from '@formily/react';
import { useCollection, useCollectionManager } from '../../../collection-manager';
/**
*
* @returns
*/
export const useOperatorList = () => {
const schema = useFieldSchema();
const fieldInterface = schema['x-designer-props']?.interface;
const { name } = useCollection();
const { getCollectionFields, getInterface } = useCollectionManager();
const collectionFields = getCollectionFields(name);
if (fieldInterface) {
return getInterface(fieldInterface)?.filterable?.operators || [];
}
const field = collectionFields.find((item) => item.name === schema.name);
return getInterface(field?.interface)?.filterable?.operators || [];
};

View File

@ -0,0 +1,45 @@
import { useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import {
EditComponent,
EditDescription,
EditOperator,
EditTitle,
EditTitleField,
EditTooltip,
EditValidationRules,
} from './SchemaSettingOptions';
export const FilterFormDesigner = () => {
const { getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
return (
<GeneralSchemaDesigner>
<EditTitle />
<EditDescription />
<EditTooltip />
<EditValidationRules />
<EditComponent />
<EditOperator />
<EditTitleField />
{collectionField ? <SchemaSettings.Divider /> : null}
<SchemaSettings.Remove
key="remove"
removeParentsIfNoChildren
confirm={{
title: t('Delete field'),
}}
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -13,6 +13,8 @@ import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'
import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks';
import { BlockItem } from '../block-item';
import { HTMLEncode } from '../input/shared';
import { FilterFormDesigner } from './FormItem.FilterFormDesigner';
import { useEnsureOperatorsValid } from './SchemaSettingOptions';
const divWrap = (schema: ISchema) => {
return {
@ -25,9 +27,12 @@ const divWrap = (schema: ISchema) => {
};
export const FormItem: any = observer((props: any) => {
useEnsureOperatorsValid();
const field = useField();
const ctx = useContext(BlockRequestContext);
const schema = useFieldSchema();
useEffect(() => {
if (ctx?.block === 'form') {
ctx.field.data = ctx.field.data || {};
@ -62,8 +67,8 @@ export const FormItem: any = observer((props: any) => {
);
});
FormItem.Designer = (props) => {
const { getCollectionFields, getCollection, getInterface, getCollectionJoinField } = useCollectionManager();
FormItem.Designer = () => {
const { getCollectionFields, getInterface, getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const tk = useFilterByTk();
const { form } = useFormBlockContext();
@ -537,3 +542,5 @@ FormItem.Designer = (props) => {
</GeneralSchemaDesigner>
);
};
FormItem.FilterFormDesigner = FilterFormDesigner;

View File

@ -0,0 +1,598 @@
import { ArrayCollapse, FormLayout } from '@formily/antd';
import { Field } from '@formily/core';
import { ISchema, Schema, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useFilterByTk, useFormBlockContext } from '../../../block-provider';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { SchemaSettings } from '../../../schema-settings';
import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks';
import { useOperatorList } from '../filter/useOperators';
export const findFilterOperators = (schema: Schema) => {
while (schema) {
if (schema['x-filter-operators']) {
return {
operators: schema['x-filter-operators'],
uid: schema['x-uid'],
};
}
schema = schema.parent;
}
return {};
};
const divWrap = (schema: ISchema) => {
return {
type: 'void',
'x-component': 'div',
properties: {
[schema.name || uid()]: schema,
},
};
};
export const EditTitle = () => {
const { getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn } = useDesignable();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
return collectionField ? (
<SchemaSettings.ModalItem
key="edit-field-title"
title={t('Edit field title')}
schema={
{
type: 'object',
title: t('Edit field title'),
properties: {
title: {
title: t('Field title'),
default: field?.title,
description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ title }) => {
if (title) {
field.title = title;
fieldSchema.title = title;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
title: fieldSchema.title,
},
});
}
dn.refresh();
}}
/>
) : null;
};
export const EditDescription = () => {
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn } = useDesignable();
return !field.readPretty ? (
<SchemaSettings.ModalItem
key="edit-description"
title={t('Edit description')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
description: {
// title: t('Description'),
default: field?.description,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ description }) => {
field.description = description;
fieldSchema.description = description;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
description: fieldSchema.description,
},
});
dn.refresh();
}}
/>
) : null;
};
export const EditTooltip = () => {
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn } = useDesignable();
return field.readPretty ? (
<SchemaSettings.ModalItem
key="edit-tooltip"
title={t('Edit tooltip')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
tooltip: {
default: fieldSchema?.['x-decorator-props']?.tooltip,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ tooltip }) => {
field.decoratorProps.tooltip = tooltip;
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
dn.refresh();
}}
/>
) : null;
};
export const EditRequired = () => {
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn, refresh } = useDesignable();
return !field.readPretty && fieldSchema['x-component'] !== 'FormField' ? (
<SchemaSettings.SwitchItem
key="required"
title={t('Required')}
checked={fieldSchema.required as boolean}
onChange={(required) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
field.required = required;
fieldSchema['required'] = required;
schema['required'] = required;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
) : null;
};
export const EditValidationRules = () => {
const { getInterface, getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const { form } = useFormBlockContext();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn, refresh } = useDesignable();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const interfaceConfig = getInterface(collectionField?.interface);
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
return form && !form?.readPretty && validateSchema ? (
<SchemaSettings.ModalItem
title={t('Set validation rules')}
components={{ ArrayCollapse, FormLayout }}
schema={
{
type: 'object',
title: t('Set validation rules'),
properties: {
rules: {
type: 'array',
default: fieldSchema?.['x-validator'],
'x-component': 'ArrayCollapse',
'x-decorator': 'FormItem',
'x-component-props': {
accordion: true,
},
maxItems: 3,
items: {
type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel',
'x-component-props': {
header: '{{ t("Validation rule") }}',
},
properties: {
index: {
type: 'void',
'x-component': 'ArrayCollapse.Index',
},
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
labelStyle: {
marginTop: '6px',
},
labelCol: 8,
wrapperCol: 16,
},
properties: {
...validateSchema,
message: {
type: 'string',
title: '{{ t("Error message") }}',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {
autoSize: {
minRows: 2,
maxRows: 2,
},
},
},
},
},
remove: {
type: 'void',
'x-component': 'ArrayCollapse.Remove',
},
moveUp: {
type: 'void',
'x-component': 'ArrayCollapse.MoveUp',
},
moveDown: {
type: 'void',
'x-component': 'ArrayCollapse.MoveDown',
},
},
},
properties: {
add: {
type: 'void',
title: '{{ t("Add validation rule") }}',
'x-component': 'ArrayCollapse.Addition',
'x-reactions': {
dependencies: ['rules'],
fulfill: {
state: {
disabled: '{{$deps[0].length >= 3}}',
},
},
},
},
},
},
},
} as ISchema
}
onSubmit={(v) => {
const rules = [];
for (const rule of v.rules) {
rules.push(_.pickBy(rule, _.identity));
}
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
// return;
// if (['number'].includes(collectionField?.interface) && collectionField?.uiSchema?.['x-component-props']?.['stringMode'] === true) {
// rules['numberStringMode'] = true;
// }
if (['percent'].includes(collectionField?.interface)) {
for (const rule of rules) {
if (!!rule.maxValue || !!rule.minValue) {
rule['percentMode'] = true;
}
if (rule.percentFormat) {
rule['percentFormats'] = true;
}
}
}
const concatValidator = _.concat([], collectionField?.uiSchema?.['x-validator'] || [], rules);
field.validator = concatValidator;
fieldSchema['x-validator'] = rules;
schema['x-validator'] = rules;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
) : null;
};
export const EditDefaultValue = () => {
const { getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const { form } = useFormBlockContext();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn, refresh } = useDesignable();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
return form && !form?.readPretty && collectionField?.uiSchema?.type ? (
<SchemaSettings.ModalItem
title={t('Set default value')}
components={{ ArrayCollapse, FormLayout }}
schema={
{
type: 'object',
title: t('Set default value'),
properties: {
default: {
...collectionField.uiSchema,
name: 'default',
title: t('Default value'),
'x-decorator': 'FormItem',
default: fieldSchema.default || collectionField.defaultValue,
},
},
} as ISchema
}
onSubmit={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
if (field.value !== v.default) {
field.value = v.default;
}
fieldSchema.default = v.default;
schema.default = v.default;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
) : null;
};
export const EditComponent = () => {
const { getInterface, getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const tk = useFilterByTk();
const { form } = useFormBlockContext();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn, insertAdjacent } = useDesignable();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const interfaceConfig = getInterface(collectionField?.interface);
const fieldComponentOptions = useFieldComponentOptions();
const isSubFormAssociationField = field.address.segments.includes('__form_grid');
return form && !isSubFormAssociationField && fieldComponentOptions ? (
<SchemaSettings.SelectItem
title={t('Field component')}
options={fieldComponentOptions}
value={fieldSchema['x-component']}
onChange={(type) => {
const schema: ISchema = {
name: collectionField.name,
type: 'void',
required: fieldSchema['required'],
description: fieldSchema['description'],
default: fieldSchema['default'],
'x-decorator': 'FormItem',
'x-designer': 'FormItem.Designer',
'x-component': type,
'x-validator': fieldSchema['x-validator'],
'x-collection-field': fieldSchema['x-collection-field'],
'x-decorator-props': fieldSchema['x-decorator-props'],
'x-component-props': {
...collectionField?.uiSchema?.['x-component-props'],
...fieldSchema['x-component-props'],
},
};
interfaceConfig?.schemaInitialize?.(schema, {
field: collectionField,
block: 'Form',
readPretty: field.readPretty,
action: tk ? 'get' : null,
});
insertAdjacent('beforeBegin', divWrap(schema), {
onSuccess: () => {
dn.remove(null, {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
});
},
});
}}
/>
) : null;
};
export const EditPattern = () => {
const { getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const { form } = useFormBlockContext();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn } = useDesignable();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
let readOnlyMode = 'editable';
if (fieldSchema['x-disabled'] === true) {
readOnlyMode = 'readonly';
}
if (fieldSchema['x-read-pretty'] === true) {
readOnlyMode = 'read-pretty';
}
return form &&
!form?.readPretty &&
collectionField?.interface !== 'o2m' &&
fieldSchema?.['x-component-props']?.['pattern-disable'] != true ? (
<SchemaSettings.SelectItem
key="pattern"
title={t('Pattern')}
options={[
{ label: t('Editable'), value: 'editable' },
{ label: t('Readonly'), value: 'readonly' },
{ label: t('Easy-reading'), value: 'read-pretty' },
]}
value={readOnlyMode}
onChange={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
switch (v) {
case 'readonly': {
fieldSchema['x-read-pretty'] = false;
fieldSchema['x-disabled'] = true;
schema['x-read-pretty'] = false;
schema['x-disabled'] = true;
field.readPretty = false;
field.disabled = true;
break;
}
case 'read-pretty': {
fieldSchema['x-read-pretty'] = true;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = true;
schema['x-disabled'] = false;
field.readPretty = true;
break;
}
default: {
fieldSchema['x-read-pretty'] = false;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = false;
schema['x-disabled'] = false;
field.readPretty = false;
field.disabled = false;
break;
}
}
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
) : null;
};
/**
* operator operator
* operator FormItem
*/
export const useEnsureOperatorsValid = () => {
const fieldSchema = useFieldSchema();
const operatorList = useOperatorList();
const { operators: storedOperators } = findFilterOperators(fieldSchema);
if (storedOperators && operatorList.length && !storedOperators[fieldSchema.name]) {
storedOperators[fieldSchema.name] = operatorList[0].value;
}
};
export const EditOperator = () => {
const compile = useCompile();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn } = useDesignable();
const operatorList = useOperatorList();
const { operators: storedOperators, uid } = findFilterOperators(fieldSchema);
if (operatorList.length && !storedOperators[fieldSchema.name]) {
storedOperators[fieldSchema.name] = operatorList[0].value;
}
return operatorList.length ? (
<SchemaSettings.SelectItem
key="operator"
title={t('Operator')}
value={storedOperators[fieldSchema.name]}
options={compile(operatorList)}
onChange={(v) => {
storedOperators[fieldSchema.name] = v;
const schema: ISchema = {
['x-uid']: uid,
['x-filter-operators']: storedOperators,
};
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
) : null;
};
export const EditTitleField = () => {
const { getCollectionFields, getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn } = useDesignable();
const compile = useCompile();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const targetFields = collectionField?.target
? getCollectionFields(collectionField.target)
: getCollectionFields(collectionField?.targetCollection) ?? [];
const options = targetFields
.filter((field) => !field?.target && field.type !== 'boolean')
.map((field) => ({
value: field?.name,
label: compile(field?.uiSchema?.title) || field?.name,
}));
return options.length > 0 && fieldSchema['x-component'] === 'CollectionField' ? (
<SchemaSettings.SelectItem
key="title-field"
title={t('Title field')}
options={options}
value={field?.componentProps?.fieldNames?.label}
onChange={(label) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
const fieldNames = {
...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
...field.componentProps.fieldNames,
label,
};
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
schema['x-component-props'] = fieldSchema['x-component-props'];
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
) : null;
};

View File

@ -21,11 +21,16 @@ export const FormDesigner = () => {
const { t } = useTranslation();
const { visible } = useActionContext();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
return (
<GeneralSchemaDesigner template={template} title={title || name}>
{/* <SchemaSettings.Template componentName={'FormItem'} collectionName={name} /> */}
<SchemaSettings.BlockTitleItem />
<SchemaSettings.FormItemTemplate componentName={'FormItem'} collectionName={name} resourceName={defaultResource} />
<SchemaSettings.FormItemTemplate
componentName={'FormItem'}
collectionName={name}
resourceName={defaultResource}
/>
<SchemaSettings.LinkageRules collectionName={name} />
<SchemaSettings.Divider />
<SchemaSettings.Remove

View File

@ -0,0 +1,39 @@
import { useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCollection } from '../../../collection-manager';
import { FilterBlockType } from '../../../filter-provider/utils';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
/**
* 使
* @returns
*/
export const FilterDesigner = () => {
const { name, title } = useCollection();
const template = useSchemaTemplate();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
return (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem />
<SchemaSettings.FormItemTemplate
componentName={'FilterFormItem'}
collectionName={name}
resourceName={defaultResource}
/>
<SchemaSettings.LinkageRules collectionName={name} />
<SchemaSettings.ConnectDataBlocks type={FilterBlockType.FORM} emptyDescription={t('No blocks to connect')} />
<SchemaSettings.Divider />
<SchemaSettings.Remove
removeParentsIfNoChildren
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -162,7 +162,8 @@ const WithoutForm = (props) => {
);
};
export const Form: React.FC<FormProps> & { Designer?: any; ReadPrettyDesigner?: any } = observer((props) => {
export const Form: React.FC<FormProps> & { Designer?: any; FilterDesigner?: any; ReadPrettyDesigner?: any } = observer(
(props) => {
const field = useField<Field>();
const { form, disabled, ...others } = useProps(props);
const formDisabled = disabled || field.disabled;
@ -179,4 +180,5 @@ export const Form: React.FC<FormProps> & { Designer?: any; ReadPrettyDesigner?:
</form>
</ConfigProvider>
);
});
},
);

View File

@ -1,7 +1,9 @@
import { FilterDesigner } from './Form.FilterDesigner';
import { Form as FormV2 } from './Form';
import { DetailsDesigner, FormDesigner, ReadPrettyFormDesigner } from './Form.Designer';
FormV2.Designer = FormDesigner;
FormV2.FilterDesigner = FilterDesigner;
FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner;
export { FormV2, DetailsDesigner };

View File

@ -358,7 +358,7 @@ export const Grid: any = observer((props: any) => {
);
});
Grid.Row = observer((props) => {
Grid.Row = observer(() => {
const field = useField();
const fieldSchema = useFieldSchema();
const addr = field.address.toString();
@ -416,12 +416,12 @@ Grid.Col = observer((props: any) => {
const { cols = [] } = useContext(GridRowContext);
const schema = useFieldSchema();
const field = useField();
let width = '100%';
let width = '';
if (cols?.length) {
const w = schema?.['x-component-props']?.['width'] || 100 / cols.length;
width = `calc(${w}% - 24px - 24px / ${cols.length})`;
}
const { isOver, setNodeRef } = useDroppable({
const { setNodeRef } = useDroppable({
id: field.address.toString(),
data: {
insertAdjacent: 'beforeEnd',
@ -431,17 +431,7 @@ Grid.Col = observer((props: any) => {
});
return (
<GridColContext.Provider value={{ cols, schema }}>
<div
ref={setNodeRef}
style={{ width }}
className={cls(
'nb-grid-col',
css`
position: relative;
/* z-index: 0; */
`,
)}
>
<div ref={setNodeRef} style={{ width }} className={cls('nb-grid-col')}>
{props.children}
</div>
</GridColContext.Provider>

View File

@ -7,6 +7,7 @@ export * from './checkbox';
export * from './color-select';
export * from './cron';
export * from './date-picker';
export * from './details';
export * from './filter';
export * from './form';
export * from './form-item';

View File

@ -104,36 +104,31 @@ const FixedBlock: React.FC<FixedBlockProps> = (props) => {
{schema ? (
<div
className={css`
height: 100%;
overflow: hidden;
position: relative;
height: calc(100vh - ${height}px);
.noco-card-item {
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
.ant-card {
display: flex;
flex-direction: column;
height: 100%;
.ant-card-body {
height: 1px;
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
// padding-bottom: 0;
}
}
& .ant-spin-nested-loading {
height: 100%;
overflow: hidden;
}
& .ant-spin-container {
height: 100%;
}
}
`}
style={{
height: `calc(100vh - ${height}px)`,
}}
>
<RecursionField onlyRenderProperties={false} schema={schema} />
<RecursionField onlyRenderProperties={false} name={schema.name} schema={schema} />
</div>
) : (
props.children

View File

@ -8,6 +8,7 @@ import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useLocation } from 'react-router-dom';
import { useDocumentTitle } from '../../../document-title';
import { FilterBlockProvider } from '../../../filter-provider/FilterProvider';
import { Icon } from '../../../icon';
import { DndContext } from '../../common';
import { SortableItem } from '../../common/sortable-item';
@ -138,6 +139,7 @@ export const Page = (props) => {
const [height, setHeight] = useState(0);
return (
<FilterBlockProvider>
<div className={pageDesignerCss}>
<PageDesigner title={fieldSchema.title || title} />
<div
@ -301,5 +303,6 @@ export const Page = (props) => {
)}
</div>
</div>
</FilterBlockProvider>
);
};

View File

@ -7,7 +7,7 @@ import { reaction } from '@formily/reactive';
import { useEventListener, useMemoizedFn } from 'ahooks';
import { Table as AntdTable, TableColumnProps } from 'antd';
import { default as classNames, default as cls } from 'classnames';
import React, { RefCallback, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DndContext, useDesignable } from '../..';
import {
@ -15,7 +15,7 @@ import {
RecordProvider,
useSchemaInitializer,
useTableBlockContext,
useTableSelectorContext
useTableSelectorContext,
} from '../../../';
import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
import { extractIndex, getIdsWithChildren, isCollectionFieldComponent, isColumnComponent } from './utils';
@ -96,7 +96,7 @@ const SortableRow = (props) => {
<tr
ref={active?.id !== id ? setNodeRef : null}
{...props}
className={classNames({ [className]: active && isOver })}
className={classNames(props.className, { [className]: active && isOver })}
/>
);
};
@ -157,7 +157,7 @@ export const Table: any = observer((props: any) => {
const field = useField<ArrayField>();
const columns = useTableColumns();
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
const { pagination: pagination2, ...others2 } = useProps?.() || {};
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
const {
dragSort = false,
showIndex = true,
@ -178,6 +178,28 @@ export const Table: any = observer((props: any) => {
const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
const [expandedKeys, setExpandesKeys] = useState([]);
const [allIncludesChildren, setAllIncludesChildren] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>(field?.data?.selectedRowKeys || []);
const [selectedRow, setSelectedRow] = useState([]);
let onRow = null,
highlightRow = '';
if (onClickRow) {
onRow = (record) => {
return {
onClick: () => onClickRow(record, setSelectedRow, selectedRow),
};
};
highlightRow = css`
& > td {
background-color: #caedff !important;
}
&:hover > td {
background-color: #caedff !important;
}
`;
}
useEffect(() => {
field.setValidator((value) => {
if (requiredValidator) {
@ -294,10 +316,11 @@ export const Table: any = observer((props: any) => {
rowSelection: rowSelection
? {
type: 'checkbox',
selectedRowKeys: field?.data?.selectedRowKeys || [],
selectedRowKeys: selectedRowKeys,
onChange(selectedRowKeys: any[], selectedRows: any[]) {
field.data = field.data || {};
field.data.selectedRowKeys = selectedRowKeys;
setSelectedRowKeys(selectedRowKeys);
onRowSelectionChange?.(selectedRowKeys, selectedRows);
},
renderCell: (checked, record, index, originNode) => {
@ -396,8 +419,10 @@ export const Table: any = observer((props: any) => {
const fieldSchema = useFieldSchema();
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
const [tableHeight, setTableHeight] = useState(0);
const [headerAndPaginationHeight, setHeaderAndPaginationHeight] = useState(0);
const tableRef = useRef(null);
const containerRef = useRef<HTMLDivElement>(null);
const scroll = useMemo(() => {
return fixedBlock
? {
@ -409,22 +434,21 @@ export const Table: any = observer((props: any) => {
};
}, [fixedBlock, tableHeight, headerAndPaginationHeight]);
const elementRef = useRef<HTMLDivElement>();
const calcTableSize = () => {
if (!elementRef.current) return;
const clientRect = elementRef.current?.getBoundingClientRect();
setTableHeight(Math.ceil(clientRect?.height || 0));
};
useEventListener('resize', calcTableSize);
if (!containerRef.current || !tableRef.current) return;
setTableHeight(Math.ceil(containerRef.current.clientHeight || 0));
const mountedRef: RefCallback<HTMLDivElement> = (ref) => {
elementRef.current = ref;
calcTableSize();
const headerHeight = tableRef.current.querySelector('.ant-table-header')?.clientHeight || 0;
const paginationHeight = tableRef.current.querySelector('.ant-table-pagination')?.clientHeight || 0;
setHeaderAndPaginationHeight(Math.ceil(headerHeight + paginationHeight + 18));
};
useEffect(calcTableSize, [field.value]);
useEventListener('resize', calcTableSize);
return (
<div
ref={mountedRef}
ref={containerRef}
className={css`
height: 100%;
overflow: hidden;
@ -439,11 +463,7 @@ export const Table: any = observer((props: any) => {
>
<SortableWrapper>
<AntdTable
ref={(ref) => {
const headerHeight = ref?.querySelector('.ant-table-header')?.getBoundingClientRect().height || 0;
const paginationHeight = ref?.querySelector('.ant-table-pagination')?.getBoundingClientRect().height || 0;
setHeaderAndPaginationHeight(Math.ceil(headerHeight + paginationHeight + 16));
}}
ref={tableRef}
rowKey={rowKey ?? defaultRowKey}
{...others}
{...restProps}
@ -452,6 +472,8 @@ export const Table: any = observer((props: any) => {
onChange={(pagination, filters, sorter, extra) => {
onTableChange?.(pagination, filters, sorter, extra);
}}
onRow={onRow}
rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')}
tableLayout={'auto'}
scroll={scroll}
columns={columns}

View File

@ -6,6 +6,7 @@ import { useTableBlockContext } from '../../../block-provider';
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
import { FilterBlockType } from '../../../filter-provider/utils';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks';
@ -234,6 +235,7 @@ export const TableBlockDesigner = () => {
});
}}
/>
<SchemaSettings.ConnectDataBlocks type={FilterBlockType.TABLE} emptyDescription={t('No blocks to connect')} />
{supportTemplate && <SchemaSettings.Divider />}
{supportTemplate && (
<SchemaSettings.Template componentName={'Table'} collectionName={name} resourceName={defaultResource} />

View File

@ -6,7 +6,7 @@ function toSchema(schema?: any) {
if (Schema.isSchemaInstance(schema)) {
return schema;
}
if (schema.name) {
if (schema?.name) {
return new Schema({
type: 'object',
properties: {

View File

@ -44,6 +44,25 @@ export const BlockInitializers = {
},
],
},
{
key: 'filterBlocks',
type: 'itemGroup',
title: '{{t("Filter blocks")}}',
children: [
{
key: 'filterForm',
type: 'item',
title: '{{t("Form")}}',
component: 'FilterFormBlockInitializer',
},
{
key: 'filterCollapse',
type: 'item',
title: '{{t("Collapse")}}',
component: 'FilterCollapseBlockInitializer',
},
],
},
{
key: 'media',
type: 'itemGroup',

View File

@ -0,0 +1,29 @@
// 表单的操作配置
export const FilterFormActionInitializers = {
title: '{{t("Configure actions")}}',
icon: 'SettingOutlined',
items: [
{
type: 'itemGroup',
title: '{{t("Enable actions")}}',
children: [
{
type: 'item',
title: '{{t("Filter")}}',
component: 'CreateFilterActionInitializer',
schema: {
'x-action-settings': {},
},
},
{
type: 'item',
title: '{{t("Reset")}}',
component: 'CreateResetActionInitializer',
schema: {
'x-action-settings': {},
},
},
],
},
],
};

View File

@ -1,21 +1,25 @@
import { union } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../SchemaInitializer';
import {
gridRowColWrap,
useAssociatedFormItemInitializerFields,
useFilterAssociatedFormItemInitializerFields,
useFilterFormItemInitializerFields,
useFilterInheritsFormItemInitializerFields,
useFormItemInitializerFields,
useInheritsFormItemInitializerFields,
} from '../utils';
import { useCompile } from '../../schema-component';
// 表单里配置字段
export const FormItemInitializers = (props: any) => {
const { t } = useTranslation();
const { insertPosition, component } = props;
const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
const associationFields = useAssociatedFormItemInitializerFields({
readPretty: true,
block: 'Form',
});
const inheritFields = useInheritsFormItemInitializerFields();
const compile = useCompile();
const fieldItems: any[] = [
@ -27,7 +31,8 @@ export const FormItemInitializers = (props: any) => {
];
if (inheritFields?.length > 0) {
inheritFields.forEach((inherit) => {
Object.values(inherit)[0].length&&fieldItems.push(
Object.values(inherit)[0].length &&
fieldItems.push(
{
type: 'divider',
},
@ -82,3 +87,76 @@ export const FormItemInitializers = (props: any) => {
/>
);
};
export const FilterFormItemInitializers = (props: any) => {
const { t } = useTranslation();
const { insertPosition, component } = props;
const associationFields = useFilterAssociatedFormItemInitializerFields();
const inheritFields = useFilterInheritsFormItemInitializerFields();
const compile = useCompile();
const fieldItems: any[] = [
{
type: 'itemGroup',
title: t('Display fields'),
children: useFilterFormItemInitializerFields(),
},
];
if (inheritFields?.length > 0) {
inheritFields.forEach((inherit) => {
Object.values(inherit)[0].length &&
fieldItems.push(
{
type: 'divider',
},
{
type: 'itemGroup',
title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
children: Object.values(inherit)[0],
},
);
});
}
associationFields.length > 0 &&
fieldItems.push(
{
type: 'divider',
},
{
type: 'itemGroup',
title: t('Display association fields'),
children: associationFields,
},
);
fieldItems.push(
{
type: 'divider',
},
{
type: 'item',
title: t('Add text'),
component: 'BlockInitializer',
schema: {
type: 'void',
'x-editable': false,
'x-decorator': 'FormItem',
'x-designer': 'Markdown.Void.Designer',
'x-component': 'Markdown.Void',
'x-component-props': {
content: t('This is a demo text, **supports Markdown syntax**.'),
},
},
},
);
return (
<SchemaInitializer.Button
wrap={gridRowColWrap}
icon={'SettingOutlined'}
items={fieldItems}
insertPosition={insertPosition}
component={component}
title={component ? null : t('Configure fields')}
/>
);
};

View File

@ -80,7 +80,7 @@ export const TableColumnInitializers = (props: any) => {
},
};
}}
items={itemsMerge(fieldItems, items)}
items={itemsMerge(fieldItems)}
>
{t('Configure columns')}
</SchemaInitializer.Button>

View File

@ -6,6 +6,7 @@ export * from './CreateFormBulkEditBlockInitializers';
export * from './CustomFormItemInitializers';
export * from './DetailsActionInitializers';
export * from './FormActionInitializers';
export * from './FilterFormActionInitializers';
export * from './FormItemInitializers';
export * from './KanbanActionInitializers';
export * from './ReadPrettyFormActionInitializers';

View File

@ -6,6 +6,7 @@ export {
gridRowColWrap,
useRecordCollectionDataSourceItems,
createTableBlockSchema,
createFilterFormBlockSchema,
useAssociatedTableColumnInitializerFields,
useInheritsTableColumnInitializerFields,
useTableColumnInitializerFields,

View File

@ -0,0 +1,17 @@
import React from 'react';
import { ActionInitializer } from './ActionInitializer';
export const CreateFilterActionInitializer = (props) => {
const schema = {
title: '{{ t("Filter") }}',
'x-action': 'submit',
'x-component': 'Action',
'x-designer': 'Action.Designer',
'x-component-props': {
type: 'primary',
useProps: '{{ useFilterBlockActionProps }}',
},
};
return <ActionInitializer {...props} schema={schema} />;
};

View File

@ -0,0 +1,15 @@
import React from 'react';
import { ActionInitializer } from './ActionInitializer';
export const CreateResetActionInitializer = (props) => {
const schema = {
title: '{{ t("Reset") }}',
'x-component': 'Action',
'x-designer': 'Action.Designer',
'x-component-props': {
useProps: '{{ useResetBlockActionProps }}',
},
};
return <ActionInitializer {...props} schema={schema} />;
};

View File

@ -0,0 +1,33 @@
import { TableOutlined } from '@ant-design/icons';
import React, { useContext } from 'react';
import { SchemaInitializer, SchemaInitializerButtonContext } from '..';
import { useSchemaTemplateManager } from '../../schema-templates';
import { useCollectionDataSourceItems } from '../utils';
export const FilterBlockInitializer = (props) => {
const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props;
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
const { setVisible } = useContext(SchemaInitializerButtonContext);
return (
<SchemaInitializer.Item
icon={<TableOutlined />}
{...others}
onClick={async ({ item }) => {
if (item.template) {
const s = await getTemplateSchemaByMode(item);
templateWrap ? insert(templateWrap(s, { item })) : insert(s);
} else {
if (onCreateBlockSchema) {
onCreateBlockSchema({ item });
} else if (createBlockSchema) {
insert(createBlockSchema({ collection: item.name }));
}
}
setVisible(false);
}}
items={useCollectionDataSourceItems(componentType)}
/>
);
};

View File

@ -0,0 +1,24 @@
import React from 'react';
import { TableOutlined } from '@ant-design/icons';
import { DataBlockInitializer } from './DataBlockInitializer';
import { createCollapseBlockSchema } from '../utils';
export const FilterCollapseBlockInitializer = (props) => {
const { insert } = props;
return (
<DataBlockInitializer
{...props}
icon={<TableOutlined />}
componentType={'FilterCollapse'}
onCreateBlockSchema={async ({ item }) => {
const schema = createCollapseBlockSchema({
collection: item.name,
// 与数据区块做区分
blockType: 'filter',
});
insert(schema);
}}
/>
);
};

View File

@ -0,0 +1,25 @@
import React from 'react';
import { FormOutlined } from '@ant-design/icons';
import { createFilterFormBlockSchema } from '../utils';
import { FilterBlockInitializer } from './FilterBlockInitializer';
export const FilterFormBlockInitializer = (props) => {
return (
<FilterBlockInitializer
{...props}
icon={<FormOutlined />}
componentType={'FilterFormItem'}
templateWrap={(templateSchema, { item }) => {
const s = createFilterFormBlockSchema({
template: templateSchema,
collection: item.name,
});
if (item.template && item.mode === 'reference') {
s['x-template-key'] = item.template.key;
}
return s;
}}
createBlockSchema={createFilterFormBlockSchema}
/>
);
};

View File

@ -13,6 +13,8 @@ export * from './CreateActionInitializer';
export * from './CreateFormBlockInitializer';
export * from './CreateFormBulkEditBlockInitializer';
export * from './CreateSubmitActionInitializer';
export * from './CreateFilterActionInitializer';
export * from './CreateResetActionInitializer';
export * from './CustomizeActionInitializer';
export * from './CustomizeBulkEditActionInitializer';
export * from './DataBlockInitializer';
@ -20,6 +22,7 @@ export * from './DeleteEventActionInitializer';
export * from './DestroyActionInitializer';
export * from './DetailsBlockInitializer';
export * from './FilterActionInitializer';
export * from './FilterFormBlockInitializer';
export * from './FormBlockInitializer';
export * from './G2PlotInitializer';
export * from './InitializerWithSwitch';
@ -37,6 +40,7 @@ export * from './RefreshActionInitializer';
export * from './SubmitActionInitializer';
export * from './TableActionColumnInitializer';
export * from './TableBlockInitializer';
export * from './FilterCollapseBlockInitializer';
export * from './TableCollectionFieldInitializer';
export * from './TableSelectorInitializer';
export * from './UpdateActionInitializer';

View File

@ -3,12 +3,13 @@ import { uid } from '@formily/shared';
import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { BlockRequestContext, SchemaInitializerItemOptions } from '../';
import { useCollection, useCollectionManager } from '../collection-manager';
import { FieldOptions, useCollection, useCollectionManager } from '../collection-manager';
import { isAssocField } from '../filter-provider/utils';
import { useActionContext, useDesignable } from '../schema-component';
import { useSchemaTemplateManager } from '../schema-templates';
import { SelectCollection } from './SelectCollection';
export const itemsMerge = (items1, items2) => {
export const itemsMerge = (items1) => {
return items1;
};
@ -197,9 +198,8 @@ export const useFormItemInitializerFields = (options?: any) => {
const { getInterface } = useCollectionManager();
const form = useForm();
const { readPretty = form.readPretty, block = 'Form' } = options || {};
const actionCtx = useActionContext();
const action = actionCtx?.fieldSchema?.['x-action'];
const { snapshot } = useActionContext();
const { snapshot, fieldSchema } = useActionContext();
const action = fieldSchema?.['x-action'];
return currentFields
?.filter((field) => field?.interface && !field?.isForeignKey && !field?.treeChildren)
@ -237,6 +237,56 @@ export const useFormItemInitializerFields = (options?: any) => {
});
};
// 筛选表单相关
export const useFilterFormItemInitializerFields = (options?: any) => {
const { name, currentFields } = useCollection();
const { getInterface } = useCollectionManager();
const form = useForm();
const { readPretty = form.readPretty, block = 'FilterForm' } = options || {};
const { snapshot, fieldSchema } = useActionContext();
const action = fieldSchema?.['x-action'];
return currentFields
?.filter((field) => field?.interface && !field?.isForeignKey && getInterface(field.interface)?.filterable)
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
let schema = {
type: 'string',
name: field.name,
required: false,
'x-designer': 'FormItem.FilterFormDesigner',
'x-component': field.interface === 'o2m' && !snapshot ? 'TableField' : 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`,
'x-component-props': {},
};
if (isAssocField(field)) {
schema = {
type: 'string',
name: field.name,
required: false,
'x-designer': 'AssociationSelect.FilterDesigner',
'x-component': 'AssociationSelect',
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`,
'x-component-props': field.uiSchema?.['x-component-props'],
};
}
const resultItem = {
type: 'item',
title: field?.uiSchema?.title || field.name,
component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, { field, block, readPretty, action });
},
schema,
} as SchemaInitializerItemOptions;
return resultItem;
});
};
export const useAssociatedFormItemInitializerFields = (options?: any) => {
const { name, fields } = useCollection();
const { getInterface, getCollectionFields } = useCollectionManager();
@ -288,6 +338,57 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
return groups;
};
const getItem = (field: FieldOptions, schemaName: string, collectionName: string, getCollectionFields) => {
if (field.interface === 'm2o') {
const subFields = getCollectionFields(field.target);
return {
type: 'subMenu',
title: field.uiSchema?.title,
children: subFields
.map((subField) => getItem(subField, `${schemaName}.${subField.name}`, collectionName, getCollectionFields))
.filter(Boolean),
} as SchemaInitializerItemOptions;
}
if (isAssocField(field)) return null;
const schema = {
type: 'string',
name: schemaName,
'x-designer': 'FormItem.FilterFormDesigner',
'x-designer-props': {
// 在 useOperatorList 中使用,用于获取对应的操作符列表
interface: field.interface,
},
'x-component': 'CollectionField',
'x-read-pretty': false,
'x-decorator': 'FormItem',
'x-collection-field': `${collectionName}.${schemaName}`,
};
return {
type: 'item',
title: field.uiSchema?.title || field.name,
component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schema,
} as SchemaInitializerItemOptions;
};
// 筛选表单相关
export const useFilterAssociatedFormItemInitializerFields = () => {
const { name, fields } = useCollection();
const { getCollectionFields } = useCollectionManager();
const interfaces = ['m2o'];
const groups = fields
?.filter((field) => {
return interfaces.includes(field.interface);
})
?.map((field) => getItem(field, field.name, name, getCollectionFields));
return groups;
};
export const useInheritsFormItemInitializerFields = (options?) => {
const { name } = useCollection();
const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = useCollectionManager();
@ -329,6 +430,50 @@ export const useInheritsFormItemInitializerFields = (options?) => {
};
});
};
// 筛选表单相关
export const useFilterInheritsFormItemInitializerFields = (options?) => {
const { name } = useCollection();
const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = useCollectionManager();
const inherits = getInheritCollections(name);
const { snapshot } = useActionContext();
return inherits?.map((v) => {
const fields = getParentCollectionFields(v, name);
const form = useForm();
const { readPretty = form.readPretty, block = 'Form' } = options || {};
const targetCollection = getCollection(v);
return {
[targetCollection.title]: fields
?.filter((field) => field?.interface && !field?.isForeignKey && getInterface(field.interface)?.filterable)
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
const schema = {
type: 'string',
name: field.name,
title: field?.uiSchema?.title || field.name,
required: false,
'x-designer': 'FormItem.FilterFormDesigner',
'x-component': field.interface === 'o2m' && !snapshot ? 'TableField' : 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`,
'x-component-props': {},
'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
};
return {
type: 'item',
title: field?.uiSchema?.title || field.name,
component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, { field, block, readPretty });
},
schema,
} as SchemaInitializerItemOptions;
}),
};
});
};
export const useCustomFormItemInitializerFields = (options?: any) => {
const { name, currentFields } = useCollection();
const { getInterface } = useCollectionManager();
@ -434,7 +579,6 @@ export const useCurrentSchema = (action: string, key: string, find = findSchema,
const { remove } = useDesignable();
const schema = find(fieldSchema, key, action);
const ctx = useContext(BlockRequestContext);
const exists = !!schema;
return {
schema,
@ -658,7 +802,7 @@ export const createDetailsBlockSchema = (options) => {
properties: {
[uid()]: {
type: 'void',
'x-component': 'FormV2',
'x-component': 'Details',
'x-read-pretty': true,
'x-component-props': {
useProps: '{{ useDetailsBlockProps }}',
@ -760,6 +904,67 @@ export const createFormBlockSchema = (options) => {
return schema;
};
export const createFilterFormBlockSchema = (options) => {
const {
formItemInitializers = 'FilterFormItemInitializers',
actionInitializers = 'FilterFormActionInitializers',
collection,
resource,
association,
action,
template,
...others
} = options;
const resourceName = resource || association || collection;
const schema: ISchema = {
type: 'void',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
...others,
action,
resource: resourceName,
collection,
association,
},
'x-designer': 'FormV2.FilterDesigner',
'x-component': 'CardItem',
// 保存当前筛选区块所能过滤的数据区块
'x-filter-targets': [],
// 用于存储用户设置的每个字段的运算符,目前仅筛选表单区块支持自定义
'x-filter-operators': {},
properties: {
[uid()]: {
type: 'void',
'x-component': 'FormV2',
'x-component-props': {
useProps: '{{ useFormBlockProps }}',
},
properties: {
grid: template || {
type: 'void',
'x-component': 'Grid',
'x-initializer': formItemInitializers,
properties: {},
},
actions: {
type: 'void',
'x-initializer': actionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
float: 'right',
},
},
properties: {},
},
},
},
},
};
return schema;
};
export const createReadPrettyFormBlockSchema = (options) => {
const {
formItemInitializers = 'ReadPrettyFormItemInitializers',
@ -830,6 +1035,9 @@ export const createTableBlockSchema = (options) => {
tableActionColumnInitializers,
tableBlockProvider,
disableTemplate,
TableBlockDesigner,
blockType,
pageSize = 20,
...others
} = options;
const schema: ISchema = {
@ -841,16 +1049,18 @@ export const createTableBlockSchema = (options) => {
resource: resource || collection,
action: 'list',
params: {
pageSize: 20,
pageSize,
},
rowKey,
showIndex: true,
dragSort: false,
disableTemplate: disableTemplate ?? false,
blockType,
...others,
},
'x-designer': 'TableBlockDesigner',
'x-designer': TableBlockDesigner ?? 'TableBlockDesigner',
'x-component': 'CardItem',
'x-filter-targets': [],
properties: {
actions: {
type: 'void',
@ -903,6 +1113,35 @@ export const createTableBlockSchema = (options) => {
return schema;
};
export const createCollapseBlockSchema = (options) => {
const { collection, blockType } = options;
const schema: ISchema = {
type: 'void',
'x-decorator': 'AssociationFilter.Provider',
'x-decorator-props': {
collection,
blockType,
associationFilterStyle: {
width: '100%',
},
},
'x-designer': 'AssociationFilter.BlockDesigner',
'x-component': 'CardItem',
'x-filter-targets': [],
properties: {
[uid()]: {
type: 'void',
'x-action': 'associateFilter',
'x-initializer': 'AssociationFilter.FilterBlockInitializer',
'x-component': 'AssociationFilter',
properties: {},
},
},
};
return schema;
};
export const createTableSelectorSchema = (options) => {
const { collection, resource, rowKey, ...others } = options;
const schema: ISchema = {

View File

@ -10,6 +10,7 @@ import {
Cascader,
CascaderProps,
Dropdown,
Empty,
Menu,
MenuItemProps,
Modal,
@ -19,7 +20,7 @@ import {
} from 'antd';
import classNames from 'classnames';
import { cloneDeep } from 'lodash';
import React, { createContext, useContext, useMemo, useState } from 'react';
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import {
@ -43,7 +44,10 @@ import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
import { FormLinkageRules } from './LinkageRules';
import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks';
import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils';
import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks';
import { EnableChildCollections } from './EnableChildCollections';
import { getTargetKey } from '../schema-component/antd/association-filter/utilts';
interface SchemaSettingsProps {
title?: any;
@ -448,8 +452,142 @@ SchemaSettings.Remove = (props: any) => {
);
};
SchemaSettings.ConnectDataBlocks = (props: { type: FilterBlockType; emptyDescription?: string }) => {
const { type, emptyDescription } = props;
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const { t } = useTranslation();
const collection = useCollection();
const dataBlocks = useSupportedBlocks(type);
let { targets = [], uid } = findFilterTargets(fieldSchema);
const compile = useCompile();
const Content = dataBlocks.map((block) => {
const title = `${compile(block.collection.title)} #${block.uid.slice(0, 4)}`;
const onHover = () => {
const dom = block.dom;
const designer = dom.querySelector('.general-schema-designer') as HTMLElement;
if (designer) {
designer.style.display = 'block';
}
dom.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.2)';
dom.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
};
const onLeave = () => {
const dom = block.dom;
const designer = dom.querySelector('.general-schema-designer') as HTMLElement;
if (designer) {
designer.style.display = null;
}
dom.style.boxShadow = 'none';
};
if (isSameCollection(block.collection, collection)) {
return (
<SchemaSettings.SwitchItem
key={block.uid}
title={title}
checked={targets.some((target) => target.uid === block.uid)}
onChange={(checked) => {
if (checked) {
targets.push({ uid: block.uid });
} else {
targets = targets.filter((target) => target.uid !== block.uid);
}
updateFilterTargets(fieldSchema, targets);
dn.emit('patch', {
schema: {
['x-uid']: uid,
'x-filter-targets': targets,
},
});
dn.refresh();
}}
onMouseEnter={onHover}
onMouseLeave={onLeave}
/>
);
}
const target = targets.find((target) => target.uid === block.uid);
// 与筛选区块的数据表具有关系的表
return (
<SchemaSettings.SelectItem
key={block.uid}
title={title}
value={target?.field || ''}
options={[
...block.associatedFields
.filter((field) => field.target === collection.name)
.map((field) => {
return {
label: compile(field.uiSchema.title) || field.name,
value: `${field.name}.${getTargetKey(field)}`,
};
}),
{
label: t('Unconnected'),
value: '',
},
]}
onChange={(value) => {
if (value === '') {
targets = targets.filter((target) => target.uid !== block.uid);
} else {
targets = targets.filter((target) => target.uid !== block.uid);
targets.push({ uid: block.uid, field: value });
}
updateFilterTargets(fieldSchema, targets);
dn.emit('patch', {
schema: {
['x-uid']: uid,
'x-filter-targets': targets,
},
});
dn.refresh();
}}
onClick={(e) => e.stopPropagation()}
onMouseEnter={onHover}
onMouseLeave={onLeave}
/>
);
});
return (
<SchemaSettings.SubMenu title={t('Connect data blocks')}>
{Content.length ? (
Content
) : (
<Empty
style={{ width: 160, padding: '0 1em' }}
description={emptyDescription}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</SchemaSettings.SubMenu>
);
};
SchemaSettings.SelectItem = (props) => {
const { title, options, value, onChange, ...others } = props;
const { title, options, value, onChange, openOnHover, onClick: _onClick, ...others } = props;
const [open, setOpen] = useState(false);
const onClick = (...args) => {
setOpen(false);
_onClick?.(...args);
};
// 鼠标 hover 时,打开下拉框
const moreProps = openOnHover
? {
onMouseEnter: useCallback(() => setOpen(true), []),
open,
}
: {};
return (
<SchemaSettings.Item {...others}>
<div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>
@ -457,9 +595,11 @@ SchemaSettings.SelectItem = (props) => {
<Select
bordered={false}
defaultValue={value}
onChange={onChange}
onChange={(...arg) => (setOpen(false), onChange(...arg))}
options={options}
style={{ textAlign: 'right', minWidth: 100 }}
onClick={onClick}
{...moreProps}
/>
</div>
</SchemaSettings.Item>
@ -513,7 +653,6 @@ SchemaSettings.PopupItem = (props) => {
const { schema, ...others } = props;
const [visible, setVisible] = useState(false);
const ctx = useContext(SchemaSettingsContext);
const actx = useActionContext();
return (
<ActionContext.Provider value={{ visible, setVisible }}>
<SchemaSettings.Item

View File

@ -1,5 +1,6 @@
import deepmerge from 'deepmerge';
import lodash from 'lodash';
import { isPlainObject } from './common';
type MergeStrategyType = 'merge' | 'deepMerge' | 'overwrite' | 'andMerge' | 'orMerge' | 'intersect' | 'union';
type MergeStrategyFunc = (x: any, y: any) => any;
@ -10,15 +11,6 @@ export interface MergeStrategies {
[key: string]: MergeStrategy;
}
export default function isPlainObject(value) {
if (Object.prototype.toString.call(value) !== '[object Object]') {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === null || prototype === Object.prototype;
}
function getEnumerableOwnPropertySymbols(target: any): any[] {
return Object.getOwnPropertySymbols
? Object.getOwnPropertySymbols(target).filter((symbol) => target.propertyIsEnumerable(symbol))
@ -90,7 +82,8 @@ mergeStrategies.set('union', (x, y) => {
return lodash.uniq((x || []).concat(y || [])).filter(Boolean);
});
mergeStrategies.set('intersect', (x, y) => (() => {
mergeStrategies.set('intersect', (x, y) =>
(() => {
if (typeof x === 'string') {
x = x.split(',');
}
@ -104,7 +97,8 @@ mergeStrategies.set('intersect', (x, y) => (() => {
return x || [];
}
return x.filter((v) => y.includes(v));
})().filter(Boolean));
})().filter(Boolean),
);
export function assign(target: any, source: any, strategies: MergeStrategies = {}) {
getKeys(source).forEach((sourceKey) => {

View File

@ -5,4 +5,5 @@ export * from './number';
export * from './registry';
// export * from './toposort';
export * from './uid';
export * from './common';

View File

@ -0,0 +1,18 @@
export const isEmpty = (value: unknown) => {
if (isPlainObject(value)) {
return Object.keys(value).length === 0;
}
if (Array.isArray(value)) {
return value.length === 0;
}
return !value;
};
export const isPlainObject = (value) => {
if (Object.prototype.toString.call(value) !== '[object Object]') {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === null || prototype === Object.prototype;
};

View File

@ -9,3 +9,4 @@ export * from './toposort';
export * from './uid';
export * from './assign';
export * from './collections-graph';
export * from './common';

View File

@ -80,7 +80,7 @@ export const AuditLogsTableColumnInitializers = (props: any) => {
},
};
}}
items={itemsMerge(fieldItems, items)}
items={itemsMerge(fieldItems)}
>
{t('Configure columns')}
</SchemaInitializer.Button>

View File

@ -19,6 +19,7 @@
"Time": "Time",
"Event": "Event",
"None": "None",
"Unconnected": "Unconnected",
"System settings": "System settings",
"System title": "System title",
"Logo": "Logo",
@ -54,8 +55,10 @@
"Close": "Close",
"Set the data scope": "Set the data scope",
"Data blocks": "Data blocks",
"Filter blocks": "Filter blocks",
"Table": "Table",
"Form": "Form",
"Collapse": "Collapse",
"Select data source": "Select data source",
"Calendar": "Calendar",
"Delete events": "Delete events",
@ -103,6 +106,7 @@
"Are you sure you want to delete it?": "Are you sure you want to delete it?",
"This is a demo text, **supports Markdown syntax**.": "This is a demo text, **supports Markdown syntax**.",
"Filter": "Filter",
"Connect data blocks": "Connect data blocks",
"Action type": "Action type",
"Actions": "Actions",
"Insert": "Insert",
@ -142,6 +146,7 @@
"Association fields filter": "Association fields filter",
"PK & FK fields": "PK & FK fields",
"Association fields": "Association fields",
"Optional fields": "Optional fields",
"System fields": "System fields",
"General fields": "General fields",
"Parent collection fields": "Parent collection fields",
@ -219,6 +224,7 @@
"Edit block title": "Edit block title",
"Block title": "Block title",
"Pattern": "Pattern",
"operater": "operater",
"Editable": "Editable",
"Readonly": "Readonly",
"Easy-reading": "Easy-reading",
@ -383,6 +389,7 @@
"Convert reference to duplicate": "Convert reference to duplicate",
"Template name": "Template name",
"Block type": "Block type",
"No blocks to connect": "No blocks to connect",
"Action column": "Action column",
"Records per page": "Records per page",
"(Fields only)": "(Fields only)",
@ -924,6 +931,7 @@
"Delete field": "Delete field",
"Required": "Required",
"Pattern": "Pattern",
"operater": "operater",
"Editable": "Editable",
"Readonly": "Readonly",
"Easy-reading": "Easy-reading",

View File

@ -19,6 +19,7 @@
"Time": "时间",
"Event": "事件",
"None": "无",
"Unconnected": "未连接",
"System settings": "系统设置",
"System title": "系统名称",
"Logo": "Logo",
@ -54,8 +55,10 @@
"Close": "关闭",
"Set the data scope": "设置数据范围",
"Data blocks": "数据区块",
"Filter blocks": "筛选区块",
"Table": "表格",
"Form": "表单",
"Collapse": "折叠面板",
"Select data source": "选择数据源",
"Calendar": "日历",
"Delete events": "删除日程",
@ -103,6 +106,7 @@
"Are you sure you want to delete it?": "你确定要删除吗?",
"This is a demo text, **supports Markdown syntax**.": "这是一段演示文本,**支持 Markdown 语法**。",
"Filter": "筛选",
"Connect data blocks": "连接数据区块",
"Action type": "操作类型",
"Actions": "操作",
"Insert": "新增",
@ -142,6 +146,7 @@
"Association fields filter": "关系筛选",
"PK & FK fields": "主外键字段",
"Association fields": "关系字段",
"Optional fields": "可选字段",
"System fields": "系统字段",
"General fields": "普通字段",
"Parent collection fields": "父表字段",
@ -219,6 +224,7 @@
"Edit block title": "编辑区块标题",
"Block title": "区块标题",
"Pattern": "模式",
"operater": "运算符",
"Editable": "可编辑",
"Readonly": "只读(禁止编辑)",
"Easy-reading": "只读(阅读模式)",
@ -383,6 +389,7 @@
"Convert reference to duplicate": "模板引用转为复制",
"Template name": "模板名称",
"Block type": "区块类型",
"No blocks to connect": "没有可连接的区块",
"Action column": "操作列",
"Records per page": "每页显示数量",
"(Fields only)": "(仅字段)",
@ -918,6 +925,7 @@
"Delete field": "删除字段",
"Required": "必填",
"Pattern": "模式",
"operater": "运算符",
"Editable": "可编辑",
"Readonly": "只读(禁止编辑)",
"Easy-reading": "只读(阅读模式)",

View File

@ -6,6 +6,7 @@ export default {
'Create Collection': 'Create Collection',
'All Fields': 'All Fields',
'Association Fields': 'Association Fields',
'Optional fields': 'Optional fields',
'All relationships': 'All relationships',
'Entity relationship only': 'Entity relationship only',
'Inheritance relationship only': 'Inheritance relationship only',

View File

@ -6,6 +6,7 @@ export default {
'Create Collection':'创建数据表',
'All Fields': '全部',
'Association Fields': '关系字段',
'Optional fields': '可选字段',
'All relationships': '所有关系',
'Entity relationship only': '实体关系',
'Inheritance relationship only': '继承关系',

View File

@ -36,6 +36,7 @@ const locale = {
'Delete field': '删除字段',
Required: '必填',
Pattern: '模式',
"Operator": "运算符",
Editable: '可编辑',
Readonly: '只读(禁止编辑)',
'Easy-reading': '只读(阅读模式)',