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:
parent
ef47cac5e6
commit
070c9c21ec
@ -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({
|
||||
|
@ -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>
|
||||
|
@ -53,7 +53,9 @@ export const useDetailsBlockProps = () => {
|
||||
const ctx = useDetailsBlockContext();
|
||||
useEffect(() => {
|
||||
if (!ctx.service.loading) {
|
||||
ctx.form.setValues(ctx.service?.data?.data?.[0] || {});
|
||||
ctx.form.reset().then(() => {
|
||||
ctx.form.setValues(ctx.service?.data?.data?.[0] || {});
|
||||
});
|
||||
}
|
||||
}, [ctx.service.loading]);
|
||||
return {
|
||||
|
@ -12,11 +12,11 @@ export const FormFieldContext = createContext<any>({});
|
||||
const InternalFormFieldProvider = (props) => {
|
||||
const { action, readPretty, fieldName } = props;
|
||||
const formBlockCtx = useFormBlockContext();
|
||||
|
||||
|
||||
if (!formBlockCtx?.updateAssociationValues?.includes(fieldName)) {
|
||||
formBlockCtx?.updateAssociationValues?.push(fieldName);
|
||||
}
|
||||
|
||||
|
||||
const field = useField();
|
||||
|
||||
const form = useMemo(
|
||||
@ -37,8 +37,6 @@ const InternalFormFieldProvider = (props) => {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
console.log('InternalFormFieldProvider', fieldName);
|
||||
|
||||
return (
|
||||
<RecordProvider record={service?.data?.data}>
|
||||
<FormFieldContext.Provider
|
||||
@ -54,14 +52,13 @@ const InternalFormFieldProvider = (props) => {
|
||||
{props.children}
|
||||
</FormFieldContext.Provider>
|
||||
</RecordProvider>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export const WithoutFormFieldResource = createContext(null);
|
||||
|
||||
export const FormFieldProvider = (props) => {
|
||||
console.log('FormFieldProvider', props);
|
||||
return (
|
||||
<WithoutFormFieldResource.Provider value={false}>
|
||||
<BlockProvider block={'FormField'} {...props}>
|
||||
@ -84,4 +81,4 @@ export const useFormFieldProps = () => {
|
||||
form: ctx.form,
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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]));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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();
|
||||
|
121
packages/core/client/src/filter-provider/FilterProvider.tsx
Normal file
121
packages/core/client/src/filter-provider/FilterProvider.tsx
Normal 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 };
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
89
packages/core/client/src/filter-provider/utils.ts
Normal file
89
packages/core/client/src/filter-provider/utils.ts
Normal 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;
|
||||
};
|
@ -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)",
|
||||
|
@ -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": "読取り専用(読取りモード)",
|
||||
|
@ -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)": "(Только поля)",
|
||||
|
@ -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",
|
||||
|
@ -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)": "(仅字段)",
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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])}
|
||||
|
@ -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();
|
||||
|
@ -0,0 +1,2 @@
|
||||
// TODO: 因他们之间功能相同,所以先直接复用,后续有需要再拆分
|
||||
export { TableBlockProvider as AssociationFilterProvider } from '../../../block-provider';
|
@ -0,0 +1,5 @@
|
||||
import { CollectionFieldOptions } from '../../../collection-manager';
|
||||
|
||||
export const getTargetKey = (field?: CollectionFieldOptions) => {
|
||||
return field?.targetKey || 'id';
|
||||
};
|
@ -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;
|
||||
|
@ -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}>
|
||||
|
@ -0,0 +1,3 @@
|
||||
import { FormV2 } from '../form-v2';
|
||||
|
||||
export const Details = FormV2;
|
@ -0,0 +1 @@
|
||||
export * from './Details';
|
@ -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 }) => {
|
||||
|
@ -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 || [];
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -162,21 +162,23 @@ const WithoutForm = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Form: React.FC<FormProps> & { Designer?: any; ReadPrettyDesigner?: any } = observer((props) => {
|
||||
const field = useField<Field>();
|
||||
const { form, disabled, ...others } = useProps(props);
|
||||
const formDisabled = disabled || field.disabled;
|
||||
return (
|
||||
<ConfigProvider componentDisabled={formDisabled}>
|
||||
<form>
|
||||
<Spin spinning={field.loading || false}>
|
||||
{form ? (
|
||||
<WithForm form={form} {...others} disabled={formDisabled} />
|
||||
) : (
|
||||
<WithoutForm {...others} disabled={formDisabled} />
|
||||
)}
|
||||
</Spin>
|
||||
</form>
|
||||
</ConfigProvider>
|
||||
);
|
||||
});
|
||||
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;
|
||||
return (
|
||||
<ConfigProvider componentDisabled={formDisabled}>
|
||||
<form>
|
||||
<Spin spinning={field.loading || false}>
|
||||
{form ? (
|
||||
<WithForm form={form} {...others} disabled={formDisabled} />
|
||||
) : (
|
||||
<WithoutForm {...others} disabled={formDisabled} />
|
||||
)}
|
||||
</Spin>
|
||||
</form>
|
||||
</ConfigProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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 };
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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,168 +139,170 @@ export const Page = (props) => {
|
||||
const [height, setHeight] = useState(0);
|
||||
|
||||
return (
|
||||
<div className={pageDesignerCss}>
|
||||
<PageDesigner title={fieldSchema.title || title} />
|
||||
<div
|
||||
ref={(ref) => {
|
||||
setHeight(Math.floor(ref?.getBoundingClientRect().height || 0) + 1);
|
||||
}}
|
||||
>
|
||||
{!disablePageHeader && (
|
||||
<AntdPageHeader
|
||||
className={css`
|
||||
&.has-footer {
|
||||
padding-top: 12px;
|
||||
.ant-page-header-heading-left {
|
||||
/* margin: 0; */
|
||||
}
|
||||
.ant-page-header-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
ghost={false}
|
||||
title={hidePageTitle ? undefined : fieldSchema.title || compile(title)}
|
||||
{...others}
|
||||
footer={
|
||||
enablePageTabs && (
|
||||
<DndContext>
|
||||
<Tabs
|
||||
size={'small'}
|
||||
activeKey={activeKey}
|
||||
onTabClick={(activeKey) => {
|
||||
setLoading(true);
|
||||
setActiveKey(activeKey);
|
||||
window.history.pushState({}, '', window.location.pathname + `?tab=` + activeKey);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 50);
|
||||
}}
|
||||
tabBarExtraContent={
|
||||
dn.designable && (
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
className={css`
|
||||
border-color: rgb(241, 139, 98) !important;
|
||||
color: rgb(241, 139, 98) !important;
|
||||
`}
|
||||
type={'dashed'}
|
||||
onClick={async () => {
|
||||
const values = await FormDialog(t('Add tab'), () => {
|
||||
return (
|
||||
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Tab name'),
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
title: t('Icon'),
|
||||
'x-component': 'IconPicker',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
}).open({
|
||||
initialValues: {},
|
||||
});
|
||||
const { title, icon } = values;
|
||||
dn.insertBeforeEnd({
|
||||
type: 'void',
|
||||
title,
|
||||
'x-icon': icon,
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'BlockInitializers',
|
||||
properties: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Add tab')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
{fieldSchema.mapProperties((schema) => {
|
||||
return (
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
<SortableItem
|
||||
id={schema.name}
|
||||
schema={schema}
|
||||
className={classNames('nb-action-link', designerCss, props.className)}
|
||||
>
|
||||
{schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />}
|
||||
<span>{schema.title || t('Unnamed')}</span>
|
||||
<PageTabDesigner schema={schema} />
|
||||
</SortableItem>
|
||||
}
|
||||
key={schema.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={'m24'} style={{ margin: 24 }}>
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : !disablePageHeader && enablePageTabs ? (
|
||||
fieldSchema.mapProperties((schema) => {
|
||||
if (schema.name !== activeKey) return null;
|
||||
return (
|
||||
<FixedBlock
|
||||
key={schema.name}
|
||||
height={
|
||||
// header 46 margin 48
|
||||
height + 46 + 48
|
||||
}
|
||||
>
|
||||
<SchemaComponent
|
||||
schema={
|
||||
new Schema({
|
||||
properties: {
|
||||
[schema.name]: schema,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</FixedBlock>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<FixedBlock
|
||||
height={
|
||||
// header 46 margin 48
|
||||
height + 46 + 48
|
||||
}
|
||||
>
|
||||
<div
|
||||
<FilterBlockProvider>
|
||||
<div className={pageDesignerCss}>
|
||||
<PageDesigner title={fieldSchema.title || title} />
|
||||
<div
|
||||
ref={(ref) => {
|
||||
setHeight(Math.floor(ref?.getBoundingClientRect().height || 0) + 1);
|
||||
}}
|
||||
>
|
||||
{!disablePageHeader && (
|
||||
<AntdPageHeader
|
||||
className={css`
|
||||
> .nb-grid:not(:last-child) {
|
||||
> .nb-schema-initializer-button {
|
||||
display: none;
|
||||
&.has-footer {
|
||||
padding-top: 12px;
|
||||
.ant-page-header-heading-left {
|
||||
/* margin: 0; */
|
||||
}
|
||||
.ant-page-header-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
ghost={false}
|
||||
title={hidePageTitle ? undefined : fieldSchema.title || compile(title)}
|
||||
{...others}
|
||||
footer={
|
||||
enablePageTabs && (
|
||||
<DndContext>
|
||||
<Tabs
|
||||
size={'small'}
|
||||
activeKey={activeKey}
|
||||
onTabClick={(activeKey) => {
|
||||
setLoading(true);
|
||||
setActiveKey(activeKey);
|
||||
window.history.pushState({}, '', window.location.pathname + `?tab=` + activeKey);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 50);
|
||||
}}
|
||||
tabBarExtraContent={
|
||||
dn.designable && (
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
className={css`
|
||||
border-color: rgb(241, 139, 98) !important;
|
||||
color: rgb(241, 139, 98) !important;
|
||||
`}
|
||||
type={'dashed'}
|
||||
onClick={async () => {
|
||||
const values = await FormDialog(t('Add tab'), () => {
|
||||
return (
|
||||
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Tab name'),
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
title: t('Icon'),
|
||||
'x-component': 'IconPicker',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
}).open({
|
||||
initialValues: {},
|
||||
});
|
||||
const { title, icon } = values;
|
||||
dn.insertBeforeEnd({
|
||||
type: 'void',
|
||||
title,
|
||||
'x-icon': icon,
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'BlockInitializers',
|
||||
properties: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Add tab')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
{fieldSchema.mapProperties((schema) => {
|
||||
return (
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
<SortableItem
|
||||
id={schema.name}
|
||||
schema={schema}
|
||||
className={classNames('nb-action-link', designerCss, props.className)}
|
||||
>
|
||||
{schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />}
|
||||
<span>{schema.title || t('Unnamed')}</span>
|
||||
<PageTabDesigner schema={schema} />
|
||||
</SortableItem>
|
||||
}
|
||||
key={schema.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={'m24'} style={{ margin: 24 }}>
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : !disablePageHeader && enablePageTabs ? (
|
||||
fieldSchema.mapProperties((schema) => {
|
||||
if (schema.name !== activeKey) return null;
|
||||
return (
|
||||
<FixedBlock
|
||||
key={schema.name}
|
||||
height={
|
||||
// header 46 margin 48
|
||||
height + 46 + 48
|
||||
}
|
||||
>
|
||||
<SchemaComponent
|
||||
schema={
|
||||
new Schema({
|
||||
properties: {
|
||||
[schema.name]: schema,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</FixedBlock>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<FixedBlock
|
||||
height={
|
||||
// header 46 margin 48
|
||||
height + 46 + 48
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</FixedBlock>
|
||||
)}
|
||||
<div
|
||||
className={css`
|
||||
> .nb-grid:not(:last-child) {
|
||||
> .nb-schema-initializer-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</FixedBlock>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FilterBlockProvider>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
|
@ -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} />
|
||||
|
@ -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: {
|
||||
|
@ -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',
|
||||
|
@ -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': {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
@ -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,16 +31,17 @@ export const FormItemInitializers = (props: any) => {
|
||||
];
|
||||
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],
|
||||
},
|
||||
);
|
||||
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 &&
|
||||
@ -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')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -80,7 +80,7 @@ export const TableColumnInitializers = (props: any) => {
|
||||
},
|
||||
};
|
||||
}}
|
||||
items={itemsMerge(fieldItems, items)}
|
||||
items={itemsMerge(fieldItems)}
|
||||
>
|
||||
{t('Configure columns')}
|
||||
</SchemaInitializer.Button>
|
||||
|
@ -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';
|
||||
|
@ -6,6 +6,7 @@ export {
|
||||
gridRowColWrap,
|
||||
useRecordCollectionDataSourceItems,
|
||||
createTableBlockSchema,
|
||||
createFilterFormBlockSchema,
|
||||
useAssociatedTableColumnInitializerFields,
|
||||
useInheritsTableColumnInitializerFields,
|
||||
useTableColumnInitializerFields,
|
||||
|
@ -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} />;
|
||||
};
|
@ -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} />;
|
||||
};
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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';
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
@ -71,8 +63,8 @@ mergeStrategies.set('orMerge', (x, y) => {
|
||||
mergeStrategies.set('deepMerge', (x, y) => {
|
||||
return isPlainObject(x) && isPlainObject(y)
|
||||
? deepmerge(x, y, {
|
||||
arrayMerge: (x, y) => y,
|
||||
})
|
||||
arrayMerge: (x, y) => y,
|
||||
})
|
||||
: y;
|
||||
});
|
||||
|
||||
@ -90,21 +82,23 @@ mergeStrategies.set('union', (x, y) => {
|
||||
return lodash.uniq((x || []).concat(y || [])).filter(Boolean);
|
||||
});
|
||||
|
||||
mergeStrategies.set('intersect', (x, y) => (() => {
|
||||
if (typeof x === 'string') {
|
||||
x = x.split(',');
|
||||
}
|
||||
if (typeof y === 'string') {
|
||||
y = y.split(',');
|
||||
}
|
||||
if (!Array.isArray(x) || x.length === 0) {
|
||||
return y || [];
|
||||
}
|
||||
if (!Array.isArray(y) || y.length === 0) {
|
||||
return x || [];
|
||||
}
|
||||
return x.filter((v) => y.includes(v));
|
||||
})().filter(Boolean));
|
||||
mergeStrategies.set('intersect', (x, y) =>
|
||||
(() => {
|
||||
if (typeof x === 'string') {
|
||||
x = x.split(',');
|
||||
}
|
||||
if (typeof y === 'string') {
|
||||
y = y.split(',');
|
||||
}
|
||||
if (!Array.isArray(x) || x.length === 0) {
|
||||
return y || [];
|
||||
}
|
||||
if (!Array.isArray(y) || y.length === 0) {
|
||||
return x || [];
|
||||
}
|
||||
return x.filter((v) => y.includes(v));
|
||||
})().filter(Boolean),
|
||||
);
|
||||
|
||||
export function assign(target: any, source: any, strategies: MergeStrategies = {}) {
|
||||
getKeys(source).forEach((sourceKey) => {
|
||||
|
@ -5,4 +5,5 @@ export * from './number';
|
||||
export * from './registry';
|
||||
// export * from './toposort';
|
||||
export * from './uid';
|
||||
export * from './common';
|
||||
|
||||
|
18
packages/core/utils/src/common.ts
Normal file
18
packages/core/utils/src/common.ts
Normal 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;
|
||||
};
|
@ -9,3 +9,4 @@ export * from './toposort';
|
||||
export * from './uid';
|
||||
export * from './assign';
|
||||
export * from './collections-graph';
|
||||
export * from './common';
|
||||
|
@ -80,7 +80,7 @@ export const AuditLogsTableColumnInitializers = (props: any) => {
|
||||
},
|
||||
};
|
||||
}}
|
||||
items={itemsMerge(fieldItems, items)}
|
||||
items={itemsMerge(fieldItems)}
|
||||
>
|
||||
{t('Configure columns')}
|
||||
</SchemaInitializer.Button>
|
||||
|
@ -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",
|
||||
|
@ -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": "只读(阅读模式)",
|
||||
|
@ -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',
|
||||
|
@ -6,6 +6,7 @@ export default {
|
||||
'Create Collection':'创建数据表',
|
||||
'All Fields': '全部',
|
||||
'Association Fields': '关系字段',
|
||||
'Optional fields': '可选字段',
|
||||
'All relationships': '所有关系',
|
||||
'Entity relationship only': '实体关系',
|
||||
'Inheritance relationship only': '继承关系',
|
||||
|
@ -36,6 +36,7 @@ const locale = {
|
||||
'Delete field': '删除字段',
|
||||
Required: '必填',
|
||||
Pattern: '模式',
|
||||
"Operator": "运算符",
|
||||
Editable: '可编辑',
|
||||
Readonly: '只读(禁止编辑)',
|
||||
'Easy-reading': '只读(阅读模式)',
|
||||
|
Loading…
Reference in New Issue
Block a user