feat(collection-manager): tableOID field and collection field (#2161)

* feat: support tableOid interface

* chore: child collection filter operator

* chore: test

* refactor: support tableOid and collection field

* chore: test

* fix: refactor: collectionSelect

* refactor: support linkage from form in add child

* refactor: add child support linkage form form

* refactor: code improve

* feat: support undefined value in childIn query

* chore: test

* refactor: locale improve

* refactor: code  improve

* refactor: code  improve

* refactor: tableoid only support pg

* refactor: tableoid only support pg

* refactor: code improve

* refactor: collection operator

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: collection field support options config

* refactor: collection field support options config

* feat: tableoid migration

* fix: item.options?.inherits

---------

Co-authored-by: ChengLei Shao <chareice@live.com>
Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
katherinehhh 2023-07-04 23:16:49 +08:00 committed by GitHub
parent 637ccb0457
commit 046a0b4f4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 535 additions and 242 deletions

View File

@ -9,10 +9,10 @@ import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client'; import { useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import { useCancelAction } from '../action-hooks'; import { useCancelAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components'; import * as components from './components';
import { getOptions } from './interfaces'; import { getOptions } from './interfaces';
@ -167,13 +167,21 @@ export const AddCollectionField = (props) => {
}; };
export const AddFieldAction = (props) => { export const AddFieldAction = (props) => {
const { scope, getContainer, item: record, children, trigger, align } = props; const { scope, getContainer, item: record, children, trigger, align, database } = props;
const { getInterface, getTemplate } = useCollectionManager(); const { getInterface, getTemplate, collections } = useCollectionManager();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [targetScope, setTargetScope] = useState(); const [targetScope, setTargetScope] = useState();
const [schema, setSchema] = useState({}); const [schema, setSchema] = useState({});
const compile = useCompile(); const compile = useCompile();
const { t } = useTranslation(); const { t } = useTranslation();
const currentCollections = useMemo(() => {
return collections.map((v) => {
return {
label: compile(v.title),
value: v.name,
};
});
}, []);
const getFieldOptions = useCallback(() => { const getFieldOptions = useCallback(() => {
const { availableFieldInterfaces } = getTemplate(record.template) || {}; const { availableFieldInterfaces } = getTemplate(record.template) || {};
const { exclude, include } = availableFieldInterfaces || {}; const { exclude, include } = availableFieldInterfaces || {};
@ -185,6 +193,8 @@ export const AddFieldAction = (props) => {
children: v.children.filter((v) => { children: v.children.filter((v) => {
if (v.value === 'id') { if (v.value === 'id') {
return typeof record['autoGenId'] === 'boolean' ? record['autoGenId'] : true; return typeof record['autoGenId'] === 'boolean' ? record['autoGenId'] : true;
} else if (v.value === 'tableoid') {
return database?.dialect === 'postgres';
} else { } else {
return typeof record[v.value] === 'boolean' ? record[v.value] : true; return typeof record[v.value] === 'boolean' ? record[v.value] : true;
} }
@ -241,7 +251,6 @@ export const AddFieldAction = (props) => {
}; };
}); });
}, [getFieldOptions]); }, [getFieldOptions]);
const menu = useMemo<MenuProps>(() => { const menu = useMemo<MenuProps>(() => {
return { return {
style: { style: {
@ -286,6 +295,7 @@ export const AddFieldAction = (props) => {
record, record,
showReverseFieldConfig: true, showReverseFieldConfig: true,
targetScope, targetScope,
collections: currentCollections,
...scope, ...scope,
}} }}
/> />

View File

@ -1,29 +1,30 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Field, createForm } from '@formily/core'; import { createForm, Field } from '@formily/core';
import { FieldContext, FormContext, useField } from '@formily/react'; import { FieldContext, FormContext, useField } from '@formily/react';
import { Space, Switch, Table, TableColumnProps, Tag, Tooltip } from 'antd'; import { Space, Switch, Table, TableColumnProps, Tag, Tooltip } from 'antd';
import React, { useContext, useMemo } from 'react'; import React, { useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCurrentAppInfo } from '../../appInfo';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { Action, useAttach, useCompile } from '../../schema-component'; import { Action, useAttach, useCompile } from '../../schema-component';
import {
ResourceActionContext,
ResourceActionProvider,
useResourceActionContext,
useResourceContext,
} from '../ResourceActionProvider';
import { import {
isDeleteButtonDisabled, isDeleteButtonDisabled,
useBulkDestroyActionAndRefreshCM, useBulkDestroyActionAndRefreshCM,
useDestroyActionAndRefreshCM, useDestroyActionAndRefreshCM,
} from '../action-hooks'; } from '../action-hooks';
import { useCollectionManager } from '../hooks/useCollectionManager'; import { useCollectionManager } from '../hooks/useCollectionManager';
import {
ResourceActionContext,
ResourceActionProvider,
useResourceActionContext,
useResourceContext,
} from '../ResourceActionProvider';
import { AddCollectionField } from './AddFieldAction'; import { AddCollectionField } from './AddFieldAction';
import { EditCollectionField } from './EditFieldAction'; import { EditCollectionField } from './EditFieldAction';
import { OverridingCollectionField } from './OverridingCollectionField'; import { OverridingCollectionField } from './OverridingCollectionField';
import { collection } from './schemas/collectionFields';
import { SyncFieldsAction } from './SyncFieldsAction'; import { SyncFieldsAction } from './SyncFieldsAction';
import { ViewCollectionField } from './ViewInheritedField'; import { ViewCollectionField } from './ViewInheritedField';
import { collection } from './schemas/collectionFields';
const indentStyle = css` const indentStyle = css`
.ant-table { .ant-table {
@ -269,6 +270,9 @@ export const CollectionFields = () => {
const compile = useCompile(); const compile = useCompile();
const field = useField<Field>(); const field = useField<Field>();
const { name } = useRecord(); const { name } = useRecord();
const {
data: { database },
} = useCurrentAppInfo();
const { getInterface, getInheritCollections, getCollection, getCurrentCollectionFields } = useCollectionManager(); const { getInterface, getInheritCollections, getCollection, getCurrentCollectionFields } = useCollectionManager();
const form = useMemo(() => createForm(), []); const form = useMemo(() => createForm(), []);
const f = useAttach(form.createArrayField({ ...field.props, basePath: '' })); const f = useAttach(form.createArrayField({ ...field.props, basePath: '' }));
@ -403,7 +407,7 @@ export const CollectionFields = () => {
}), }),
[t], [t],
); );
const addProps = { type: 'primary' }; const addProps = { type: 'primary', database };
const syncProps = { type: 'primary' }; const syncProps = { type: 'primary' };
return ( return (
<ResourceActionProvider {...resourceActionProps}> <ResourceActionProvider {...resourceActionProps}>

View File

@ -3,15 +3,15 @@ import { ISchema, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set'; import set from 'lodash/set';
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import { useCancelAction, useUpdateAction } from '../action-hooks'; import { useCancelAction, useUpdateAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components'; import * as components from './components';
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => { const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
@ -136,13 +136,21 @@ export const EditCollectionField = (props) => {
export const EditFieldAction = (props) => { export const EditFieldAction = (props) => {
const { scope, getContainer, item: record, children } = props; const { scope, getContainer, item: record, children } = props;
const { getInterface } = useCollectionManager(); const { getInterface, collections } = useCollectionManager();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({}); const [schema, setSchema] = useState({});
const api = useAPIClient(); const api = useAPIClient();
const { t } = useTranslation(); const { t } = useTranslation();
const compile = useCompile(); const compile = useCompile();
const [data, setData] = useState<any>({}); const [data, setData] = useState<any>({});
const currentCollections = useMemo(() => {
return collections.map((v) => {
return {
label: compile(v.title),
value: v.name,
};
});
}, []);
return ( return (
<RecordProvider record={record}> <RecordProvider record={record}>
<ActionContextProvider value={{ visible, setVisible }}> <ActionContextProvider value={{ visible, setVisible }}>
@ -184,6 +192,7 @@ export const EditFieldAction = (props) => {
useUpdateCollectionField, useUpdateCollectionField,
useCancelAction, useCancelAction,
showReverseFieldConfig: !data?.reverseField, showReverseFieldConfig: !data?.reverseField,
collections: currentCollections,
...scope, ...scope,
}} }}
/> />

View File

@ -4,7 +4,6 @@ import omit from 'lodash/omit';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCollection, useCollectionManager } from '.'; import { useCollection, useCollectionManager } from '.';
import { useCompile } from '..';
import { useRequest } from '../api-client'; import { useRequest } from '../api-client';
import { useRecord } from '../record-provider'; import { useRecord } from '../record-provider';
import { useActionContext } from '../schema-component'; import { useActionContext } from '../schema-component';
@ -108,10 +107,28 @@ export const useChildrenCollections = (collectionName: string) => {
}); });
}; };
export const useCollectionFilterOptions = (collectionName: string) => { export const useSelfAndChildrenCollections = (collectionName: string) => {
const { getCollectionFields, getInterface, getChildrenCollections, getCollection } = useCollectionManager(); const { getChildrenCollections, getCollection } = useCollectionManager();
const compile = useCompile(); const childrenCollections = getChildrenCollections(collectionName);
const self = getCollection(collectionName);
if (!collectionName) {
return null;
}
const options = childrenCollections.map((collection: any) => {
return {
value: collection.name,
label: collection?.title || collection.name,
};
});
options.unshift({
value: self.name,
label: self?.title || self.name,
});
return options;
};
export const useCollectionFilterOptions = (collectionName: string) => {
const { getCollectionFields, getInterface } = useCollectionManager();
return useMemo(() => { return useMemo(() => {
const fields = getCollectionFields(collectionName); const fields = getCollectionFields(collectionName);
const field2option = (field, depth) => { const field2option = (field, depth) => {
@ -161,44 +178,6 @@ export const useCollectionFilterOptions = (collectionName: string) => {
return options; return options;
}; };
const options = getOptions(fields, 1); const options = getOptions(fields, 1);
const collection = getCollection(collectionName);
const childrenCollections = getChildrenCollections(collectionName);
if (childrenCollections.length > 0 && !options.find((v) => v.name == 'tableoid')) {
options.push({
name: 'tableoid',
type: 'string',
title: '{{t("Table OID(Inheritance)")}}',
schema: {
'x-component': 'Select',
enum: [{ value: collectionName, label: compile(collection.title) }].concat(
childrenCollections.map((v) => {
return {
value: v.name,
label: compile(v.title),
};
}),
),
},
operators: [
{
label: '{{t("contains")}}',
value: '$childIn',
schema: {
'x-component': 'Select',
'x-component-props': { mode: 'tags' },
},
},
{
label: '{{t("does not contain")}}',
value: '$childNotIn',
schema: {
'x-component': 'Select',
'x-component-props': { mode: 'tags' },
},
},
],
});
}
return options; return options;
}, [collectionName]); }, [collectionName]);
}; };

View File

@ -0,0 +1,37 @@
import { ISchema } from '@formily/react';
import { collectionDataSource, defaultProps, operators } from './properties';
import { IField } from './types';
export const collection: IField = {
name: 'collection',
type: 'string',
group: 'advanced',
order: 5,
title: '{{t("Collection")}}',
sortable: true,
default: {
type: 'string',
uiSchema: {
type: 'string',
'x-component': 'CollectionSelect',
},
},
availableTypes: ['string'],
hasDefaultValue: false,
properties: {
...defaultProps,
'uiSchema.enum': collectionDataSource,
},
filterable: { operators: operators.collection },
schemaInitialize(schema: ISchema, { block }) {
const props = (schema['x-component-props'] = schema['x-component-props'] || {});
props.style = {
...(props.style || {}),
width: '100%',
};
if (['Table', 'Kanban'].includes(block)) {
props['ellipsis'] = true;
}
},
};

View File

@ -1,6 +1,7 @@
export * from './checkbox'; export * from './checkbox';
export * from './checkboxGroup'; export * from './checkboxGroup';
export * from './chinaRegion'; export * from './chinaRegion';
export * from './collection';
export * from './createdAt'; export * from './createdAt';
export * from './createdBy'; export * from './createdBy';
export * from './datetime'; export * from './datetime';
@ -25,6 +26,7 @@ export * from './radioGroup';
export * from './richText'; export * from './richText';
export * from './select'; export * from './select';
export * from './subTable'; export * from './subTable';
export * from './tableoid';
export * from './textarea'; export * from './textarea';
export * from './time'; export * from './time';
export * from './updatedAt'; export * from './updatedAt';

View File

@ -368,3 +368,14 @@ export const recordPickerViewer = {
}, },
}, },
}; };
export const collectionDataSource: ISchema = {
type: 'string',
title: '{{t("Options")}}',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
multiple: true,
},
enum: '{{collections}}',
};

View File

@ -118,3 +118,54 @@ export const boolean = [
{ label: '{{t("Yes")}}', value: '$isTruly', selected: true, noValue: true }, { label: '{{t("Yes")}}', value: '$isTruly', selected: true, noValue: true },
{ label: '{{t("No")}}', value: '$isFalsy', noValue: true }, { label: '{{t("No")}}', value: '$isFalsy', noValue: true },
]; ];
export const tableoid = [
{
label: '{{t("contains")}}',
value: '$childIn',
schema: {
'x-component': 'CollectionSelect',
'x-component-props': { mode: 'tags' },
},
},
{
label: '{{t("does not contain")}}',
value: '$childNotIn',
schema: {
'x-component': 'CollectionSelect',
'x-component-props': { mode: 'tags' },
},
},
];
export const collection = [
{
label: '{{t("is")}}',
value: '$eq',
selected: true,
schema: { 'x-component': 'CollectionSelect' },
},
{
label: '{{t("is not")}}',
value: '$ne',
schema: { 'x-component': 'CollectionSelect' },
},
{
label: '{{t("contains")}}',
value: '$in',
schema: {
'x-component': 'CollectionSelect',
'x-component-props': { mode: 'tags' },
},
},
{
label: '{{t("does not contain")}}',
value: '$notIn',
schema: {
'x-component': 'CollectionSelect',
'x-component-props': { mode: 'tags' },
},
},
{ label: '{{t("is empty")}}', value: '$empty', noValue: true },
{ label: '{{t("is not empty")}}', value: '$notEmpty', noValue: true },
];

View File

@ -0,0 +1,45 @@
import { operators } from './properties';
import { IField } from './types';
export const tableoid: IField = {
name: 'tableoid',
type: 'object',
group: 'systemInfo',
order: 0,
title: '{{t("Table OID")}}',
sortable: true,
default: {
name: '__collection',
type: 'virtual',
uiSchema: {
type: 'string',
title: '{{t("Table OID")}}',
'x-component': 'CollectionSelect',
'x-component-props': {
isTableOid: true,
},
'x-read-pretty': true,
},
},
availableTypes: ['string'],
properties: {
'uiSchema.title': {
type: 'string',
title: '{{t("Field display name")}}',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
name: {
type: 'string',
title: '{{t("Field name")}}',
required: true,
'x-disabled': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
filterable: {
operators: operators.tableoid,
},
};

View File

@ -705,5 +705,6 @@ export default {
"Find by the following fields":"Find by the following fields", "Find by the following fields":"Find by the following fields",
"Create":"Create", "Create":"Create",
"Current form": "Current form", "Current form": "Current form",
"Current object":"Current object" "Current object":"Current object",
"Linkage form form data":"Linkage form form data",
}; };

View File

@ -616,5 +616,6 @@ export default {
"Find by the following fields":"次のフィールドで検索", "Find by the following fields":"次のフィールドで検索",
"Create":"新規のみ" , "Create":"新規のみ" ,
"Current form":"現在のフォーム", "Current form":"現在のフォーム",
"Current object":"現在のオブジェクト" "Current object":"現在のオブジェクト",
"Linkage form form data":"フォームデータから連動",
} }

View File

@ -790,4 +790,5 @@ export default {
"File manager": "文件管理器", "File manager": "文件管理器",
"Direct duplicate": "直接复制", "Direct duplicate": "直接复制",
"Copy into the form and continue to fill in": "复制到表单并继续填写", "Copy into the form and continue to fill in": "复制到表单并继续填写",
"Linkage form form data":"从表单数据里联动",
} }

View File

@ -63,9 +63,8 @@ const ToManyNester = observer(
insertCount: 1, insertCount: 1,
}); });
field.value.splice(index + 1, 0, {}); field.value.splice(index + 1, 0, {});
each(field.form.fields, (targetField, key) => {
each(field.form.fields, (field, key) => { if (!targetField) {
if (!field) {
delete field.form.fields[key]; delete field.form.fields[key];
} }
}); });

View File

@ -1,23 +1,39 @@
import { connect, mapReadPretty, observer } from '@formily/react'; import { connect, mapReadPretty, observer, useField } from '@formily/react';
import { Select, SelectProps, Tag } from 'antd'; import { Select, SelectProps, Tag } from 'antd';
import React from 'react'; import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCollectionManager } from '../../../collection-manager/hooks'; import { useSelfAndChildrenCollections } from '../../../collection-manager/action-hooks';
import { useCollection, useCollectionManager } from '../../../collection-manager/hooks';
import { useCompile } from '../../hooks'; import { useCompile } from '../../hooks';
import { FilterContext } from '../filter/context';
export type CollectionSelectProps = SelectProps<any, any> & { export type CollectionSelectProps = SelectProps<any, any> & {
filter?: (item: any, index: number, array: any[]) => boolean; filter?: (item: any, index: number, array: any[]) => boolean;
isTableOid?: boolean;
}; };
function useOptions({ filter }: CollectionSelectProps) { function useOptions({ filter, isTableOid }: CollectionSelectProps) {
const compile = useCompile(); const compile = useCompile();
const field: any = useField();
const ctx = useContext(FilterContext);
const collection = useCollection();
const targetCollection = isTableOid && (ctx?.collectionName || collection.name);
const inheritCollections = useSelfAndChildrenCollections(targetCollection);
const { collections = [] } = useCollectionManager(); const { collections = [] } = useCollectionManager();
const filtered = typeof filter === 'function' ? collections.filter(filter) : collections; const currentCollections = field?.dataSource
? collections.filter((v) => {
return field?.dataSource.find((i) => i.value === v.name) || field?.dataSource.includes(v.name);
})
: collections;
const filtered =
typeof filter === 'function'
? (inheritCollections || currentCollections).filter(filter)
: inheritCollections || currentCollections;
return filtered return filtered
.filter((item) => !item.hidden) .filter((item) => !item.hidden)
.map((item) => ({ .map((item) => ({
label: compile(item.title), label: compile(item.title || item.label),
value: item.name, value: item.name || item.value,
color: item.category?.color, color: item.category?.color,
})); }));
} }
@ -27,7 +43,6 @@ export const CollectionSelect = connect(
const { filter, ...others } = props; const { filter, ...others } = props;
const options = useOptions(props); const options = useOptions(props);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Select <Select
placeholder={t('Select collection')} placeholder={t('Select collection')}

View File

@ -1,14 +1,14 @@
import { ObjectField as ObjectFieldModel } from '@formily/core'; import { ObjectField as ObjectFieldModel } from '@formily/core';
import { observer, useField, useFieldSchema } from '@formily/react'; import { observer, useField, useFieldSchema } from '@formily/react';
import React from 'react'; import React, { useEffect } from 'react';
import { useRequest } from '../../../api-client'; import { useRequest } from '../../../api-client';
import { useProps } from '../../hooks/useProps'; import { useProps } from '../../hooks/useProps';
import { DatePickerProvider } from '../date-picker'; import { DatePickerProvider } from '../date-picker';
import { FilterContext } from './context';
import { FilterActionDesigner } from './Filter.Action.Designer'; import { FilterActionDesigner } from './Filter.Action.Designer';
import { FilterAction } from './FilterAction'; import { FilterAction } from './FilterAction';
import { FilterGroup } from './FilterGroup'; import { FilterGroup } from './FilterGroup';
import { SaveDefaultValue } from './SaveDefaultValue'; import { SaveDefaultValue } from './SaveDefaultValue';
import { FilterContext } from './context';
const useDef = (options) => { const useDef = (options) => {
const field = useField<ObjectFieldModel>(); const field = useField<ObjectFieldModel>();
@ -18,14 +18,17 @@ const useDef = (options) => {
export const Filter: any = observer( export const Filter: any = observer(
(props: any) => { (props: any) => {
const { useDataSource = useDef } = props; const { useDataSource = useDef } = props;
const { options, dynamicComponent, className } = useProps(props); const { options, dynamicComponent, className, collectionName } = useProps(props);
const field = useField<ObjectFieldModel>(); const field = useField<ObjectFieldModel>();
const fieldSchema = useFieldSchema(); const fieldSchema: any = useFieldSchema();
useDataSource({ useDataSource({
onSuccess(data) { onSuccess(data) {
field.dataSource = data?.data || []; field.dataSource = data?.data || [];
}, },
}); });
useEffect(() => {
field.initialValue = fieldSchema.defaultValue;
}, []);
return ( return (
<div className={className}> <div className={className}>
<DatePickerProvider value={{ utc: false }}> <DatePickerProvider value={{ utc: false }}>
@ -36,6 +39,7 @@ export const Filter: any = observer(
dynamicComponent, dynamicComponent,
options: options || field.dataSource || [], options: options || field.dataSource || [],
disabled: props.disabled, disabled: props.disabled,
collectionName,
}} }}
> >
<FilterGroup {...props} bordered={false} /> <FilterGroup {...props} bordered={false} />

View File

@ -5,8 +5,8 @@ import { Cascader, Select, Space } from 'antd';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCompile } from '../..'; import { useCompile } from '../..';
import { DynamicComponent } from './DynamicComponent';
import { RemoveConditionContext } from './context'; import { RemoveConditionContext } from './context';
import { DynamicComponent } from './DynamicComponent';
import { useValues } from './useValues'; import { useValues } from './useValues';
export const FilterItem = observer( export const FilterItem = observer(

View File

@ -8,6 +8,7 @@ export interface FilterContextProps {
dynamicComponent?: any; dynamicComponent?: any;
options?: any[]; options?: any[];
disabled?: boolean; disabled?: boolean;
collectionName?: string;
} }
export const RemoveConditionContext = createContext(null); export const RemoveConditionContext = createContext(null);

View File

@ -2,108 +2,25 @@ import { Field } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import flat from 'flat'; import flat from 'flat';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCompile } from '../..';
import { useBlockRequestContext } from '../../../block-provider'; import { useBlockRequestContext } from '../../../block-provider';
import { mergeFilter } from '../../../block-provider/SharedFilterProvider'; import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
export const useGetFilterOptions = () => { export const useGetFilterOptions = () => {
const { getCollectionFields } = useCollectionManager(); const { getCollectionFields } = useCollectionManager();
const compile = useCompile();
const { getChildrenCollections } = useCollectionManager();
const collection = useCollection();
const getFilterFieldOptions = useGetFilterFieldOptions(); const getFilterFieldOptions = useGetFilterFieldOptions();
return (collectionName) => { return (collectionName) => {
const fields = getCollectionFields(collectionName); const fields = getCollectionFields(collectionName);
const options = getFilterFieldOptions(fields); const options = getFilterFieldOptions(fields);
const childrenCollections = getChildrenCollections(collection.name);
if (childrenCollections.length > 0 && !options.find((v) => v.name == 'tableoid')) {
options.push({
name: 'tableoid',
type: 'string',
title: '{{t("Table OID(Inheritance)")}}',
schema: {
'x-component': 'Select',
enum: [{ value: collection.name, label: compile(collection.title) }].concat(
childrenCollections.map((v) => {
return {
value: v.name,
label: compile(v.title),
};
}),
),
},
operators: [
{
label: '{{t("contains")}}',
value: '$childIn',
schema: {
'x-component': 'Select',
'x-component-props': { mode: 'tags' },
},
},
{
label: '{{t("does not contain")}}',
value: '$childNotIn',
schema: {
'x-component': 'Select',
'x-component-props': { mode: 'tags' },
},
},
],
});
}
return options; return options;
}; };
}; };
export const useFilterOptions = (collectionName: string) => { export const useFilterOptions = (collectionName: string) => {
const { getCollectionFields } = useCollectionManager(); const { getCollectionFields } = useCollectionManager();
const compile = useCompile();
const { getChildrenCollections } = useCollectionManager();
const collection = useCollection();
const fields = getCollectionFields(collectionName); const fields = getCollectionFields(collectionName);
const options = useFilterFieldOptions(fields); const options = useFilterFieldOptions(fields);
const childrenCollections = getChildrenCollections(collection.name);
if (childrenCollections.length > 0 && !options.find((v) => v.name == 'tableoid')) {
options.push({
name: 'tableoid',
type: 'string',
title: '{{t("Table OID(Inheritance)")}}',
schema: {
'x-component': 'Select',
enum: [{ value: collection.name, label: compile(collection.title) }].concat(
childrenCollections.map((v) => {
return {
value: v.name,
label: compile(v.title),
};
}),
),
},
operators: [
{
label: '{{t("contains")}}',
value: '$childIn',
schema: {
'x-component': 'Select',
'x-component-props': { mode: 'tags' },
},
},
{
label: '{{t("does not contain")}}',
value: '$childNotIn',
schema: {
'x-component': 'Select',
'x-component-props': { mode: 'tags' },
},
},
],
});
}
return options; return options;
}; };

View File

@ -61,7 +61,7 @@ export const useValues = () => {
const s2 = cloneDeep(operator?.schema); const s2 = cloneDeep(operator?.schema);
field.data.schema = merge(s1, s2); field.data.schema = merge(s1, s2);
field.data.dataIndex = dataIndex; field.data.dataIndex = dataIndex;
field.data.value = operator?.noValue ? operator.default || true : null; field.data.value = operator?.noValue ? operator.default || true : undefined;
data2value(); data2value();
}, },
setOperator(operatorValue) { setOperator(operatorValue) {
@ -71,7 +71,7 @@ export const useValues = () => {
const s1 = cloneDeep(option?.schema); const s1 = cloneDeep(option?.schema);
const s2 = cloneDeep(operator?.schema); const s2 = cloneDeep(operator?.schema);
field.data.schema = merge(s1, s2); field.data.schema = merge(s1, s2);
field.data.value = operator.noValue ? operator.default || true : null; field.data.value = operator.noValue ? operator.default || true : undefined;
data2value(); data2value();
}, },
setValue(value) { setValue(value) {

View File

@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { ArrayCollapse, ArrayItems, FormLayout, FormItem as Item } from '@formily/antd'; import { ArrayCollapse, ArrayItems, FormItem as Item, FormLayout } from '@formily/antd';
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { ISchema, Schema, observer, useField, useFieldSchema } from '@formily/react'; import { ISchema, observer, Schema, useField, useFieldSchema } from '@formily/react';
import { Select } from 'antd'; import { Select } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
@ -20,9 +20,9 @@ import {
} from '../../../collection-manager'; } from '../../../collection-manager';
import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields'; import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields';
import { GeneralSchemaItems } from '../../../schema-items/GeneralSchemaItems'; import { GeneralSchemaItems } from '../../../schema-items/GeneralSchemaItems';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings'; import { GeneralSchemaDesigner, isPatternDisabled, isShowDefaultValue, SchemaSettings } from '../../../schema-settings';
import { VariableInput } from '../../../schema-settings/VariableInput/VariableInput';
import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch'; import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch';
import { VariableInput } from '../../../schema-settings/VariableInput/VariableInput';
import { isVariable, parseVariables, useVariablesCtx } from '../../common/utils/uitls'; import { isVariable, parseVariables, useVariablesCtx } from '../../common/utils/uitls';
import { SchemaComponent } from '../../core'; import { SchemaComponent } from '../../core';
import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks'; import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks';
@ -153,7 +153,7 @@ FormItem.Designer = function Designer() {
readOnlyMode = 'read-pretty'; readOnlyMode = 'read-pretty';
} }
const dataSource = useCollectionFilterOptions(collectionField?.target); const dataSource = useCollectionFilterOptions(collectionField?.target);
const defaultFilter = field.componentProps?.service?.params?.filter || {}; const defaultFilter = fieldSchema?.['x-component-props']?.service?.params?.filter || {};
const sortFields = useSortFields(collectionField?.target); const sortFields = useSortFields(collectionField?.target);
const defaultSort = field.componentProps?.service?.params?.sort || []; const defaultSort = field.componentProps?.service?.params?.sort || [];
const fieldMode = field?.componentProps?.['mode'] || (isFileField ? 'FileManager' : 'Select'); const fieldMode = field?.componentProps?.['mode'] || (isFileField ? 'FileManager' : 'Select');
@ -446,10 +446,11 @@ FormItem.Designer = function Designer() {
title: t('Set the data scope'), title: t('Set the data scope'),
properties: { properties: {
filter: { filter: {
default: defaultFilter, defaultValue: defaultFilter,
enum: dataSource, enum: dataSource,
'x-component': 'Filter', 'x-component': 'Filter',
'x-component-props': { 'x-component-props': {
collectionName: collectionField?.target,
dynamicComponent: (props) => dynamicComponent: (props) =>
FilterDynamicComponent({ FilterDynamicComponent({
...props, ...props,

View File

@ -11,7 +11,7 @@ import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
import { getInnermostKeyAndValue } from '../../common/utils/uitls'; import { getInnermostKeyAndValue } from '../../common/utils/uitls';
import { useCompile } from '../../hooks'; import { useCompile } from '../../hooks';
import { Select, defaultFieldNames } from '../select'; import { defaultFieldNames, Select } from '../select';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
import { extractFilterfield, extractValuesByPattern, generatePattern, parseVariables } from './utils'; import { extractFilterfield, extractValuesByPattern, generatePattern, parseVariables } from './utils';
const EMPTY = 'N/A'; const EMPTY = 'N/A';
@ -156,11 +156,18 @@ const InternalRemoteSelect = connect(
str = str.replace('$iteration.', `$iteration.${path.join('.')}.`); str = str.replace('$iteration.', `$iteration.${path.join('.')}.`);
} }
const parseValue = parseVariables(str, variablesCtx); const parseValue = parseVariables(str, variablesCtx);
if (Array.isArray(parseValue)) {
const filters = parseValue.map((v) => {
return JSON.parse(JSON.stringify(c).replace(jsonlogic.value, v));
});
results.push({ $or: filters });
} else {
const filterObj = JSON.parse( const filterObj = JSON.parse(
JSON.stringify(c).replace(jsonlogic.value, str.endsWith('id') ? parseValue ?? 0 : parseValue), JSON.stringify(c).replace(jsonlogic.value, str.endsWith('id') ? parseValue ?? 0 : parseValue),
); );
results.push(filterObj); results.push(filterObj);
} }
}
}); });
return { [type]: results }; return { [type]: results };
}; };
@ -173,7 +180,9 @@ const InternalRemoteSelect = connect(
pageSize: 200, pageSize: 200,
...service?.params, ...service?.params,
// search needs // search needs
filter: mergeFilter([parseFilter(field.componentProps?.service?.params?.filter) || service?.params?.filter]), filter: mergeFilter([
parseFilter(fieldSchema?.['x-component-props']?.service?.params?.filter) || service?.params?.filter,
]),
}, },
}, },
{ {
@ -221,7 +230,7 @@ const InternalRemoteSelect = connect(
}, },
} }
: {}, : {},
field.componentProps?.service?.params?.filter || service?.params?.filter, fieldSchema?.['x-component-props']?.service?.params?.filter || service?.params?.filter,
]), ]),
}); });
searchData.current = search; searchData.current = search;

View File

@ -1,4 +1,3 @@
import flat from 'flat'; import flat from 'flat';
import _, { every, findIndex, isArray, some } from 'lodash'; import _, { every, findIndex, isArray, some } from 'lodash';
import moment from 'moment'; import moment from 'moment';
@ -8,8 +7,9 @@ import jsonLogic from '../../common/utils/logic';
type VariablesCtx = { type VariablesCtx = {
/** 当前登录的用户 */ /** 当前登录的用户 */
$user: Record<string, any>; $user?: Record<string, any>;
$date: Record<string, any>; $date?: Record<string, any>;
$form?: Record<string, any>;
}; };
export const useVariablesCtx = (): VariablesCtx => { export const useVariablesCtx = (): VariablesCtx => {

View File

@ -1,6 +1,6 @@
import { DownOutlined, PlusOutlined } from '@ant-design/icons'; import { DownOutlined, PlusOutlined } from '@ant-design/icons';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react'; import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
import { Button, Dropdown, MenuProps } from 'antd'; import { Button, Dropdown, MenuProps } from 'antd';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useDesignable } from '../../'; import { useDesignable } from '../../';
@ -9,6 +9,7 @@ import { CollectionProvider, useCollection, useCollectionManager } from '../../c
import { useRecord } from '../../record-provider'; import { useRecord } from '../../record-provider';
import { ActionContextProvider, useActionContext, useCompile } from '../../schema-component'; import { ActionContextProvider, useActionContext, useCompile } from '../../schema-component';
import { linkageAction } from '../../schema-component/antd/action/utils'; import { linkageAction } from '../../schema-component/antd/action/utils';
import { parseVariables } from '../../schema-component/common/utils/uitls';
export const actionDesignerCss = css` export const actionDesignerCss = css`
position: relative; position: relative;
@ -49,7 +50,7 @@ export const actionDesignerCss = css`
} }
`; `;
const actionAclCheck = (actionPath) => { const actionAclCheck = function useAclCheck(actionPath) {
const { data, inResources, getResourceActionParams, getStrategyActionParams } = useACLRolesCheck(); const { data, inResources, getResourceActionParams, getStrategyActionParams } = useACLRolesCheck();
const recordPkValue = useRecordPkValue(); const recordPkValue = useRecordPkValue();
const collection = useCollection(); const collection = useCollection();
@ -120,14 +121,20 @@ export const CreateRecordAction = observer(
{ displayName: 'CreateRecordAction' }, { displayName: 'CreateRecordAction' },
); );
function getLinkageCollection(str, form) {
const data = parseVariables(str, { $form: form.values });
return data;
}
export const CreateAction = observer( export const CreateAction = observer(
(props: any) => { (props: any) => {
const { onClick } = props; const { onClick } = props;
const collection = useCollection(); const collection = useCollection();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const field: any = useField();
const form = useForm();
const enableChildren = fieldSchema['x-enable-children'] || []; const enableChildren = fieldSchema['x-enable-children'] || [];
const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current']; const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current'];
const field: any = useField(); const linkageFromForm = fieldSchema?.['x-component-props']?.['linkageFromForm'];
const componentType = field.componentProps.type || 'primary'; const componentType = field.componentProps.type || 'primary';
const { getChildrenCollections } = useCollectionManager(); const { getChildrenCollections } = useCollectionManager();
const totalChildCollections = getChildrenCollections(collection.name); const totalChildCollections = getChildrenCollections(collection.name);
@ -182,6 +189,7 @@ export const CreateAction = observer(
return ( return (
<div className={actionDesignerCss}> <div className={actionDesignerCss}>
{inheritsCollections?.length > 0 ? ( {inheritsCollections?.length > 0 ? (
!linkageFromForm ? (
allowAddToCurrent === undefined || allowAddToCurrent ? ( allowAddToCurrent === undefined || allowAddToCurrent ? (
<Dropdown.Button <Dropdown.Button
type={componentType} type={componentType}
@ -207,6 +215,27 @@ export const CreateAction = observer(
} }
</Dropdown> </Dropdown>
) )
) : (
<Button
type={componentType}
disabled={field.disabled}
danger={componentType === 'danger'}
icon={icon}
onClick={(info) => {
const collectionName = getLinkageCollection(linkageFromForm, form);
const targetCollection = inheritsCollections.find((v) => v.name === collectionName)
? collectionName
: collection.name;
onClick?.(targetCollection);
}}
style={{
display: !designable && field?.data?.hidden && 'none',
opacity: designable && field?.data?.hidden && 0.1,
}}
>
{props.children}
</Button>
)
) : ( ) : (
<Button <Button
type={componentType} type={componentType}

View File

@ -0,0 +1,34 @@
import { css } from '@emotion/css';
import { observer, useFieldSchema } from '@formily/react';
import React, { useEffect, useMemo } from 'react';
import { useCompile } from '../../schema-component';
import { Variable } from '.././../schema-component';
import { useFormVariable } from '../VariableInput/hooks/useFormVariable';
export const ChildDynamicComponent = observer(
(props: { collectionName: string; form: any; onChange; value; default }) => {
const { form, collectionName, onChange, value } = props;
const formVariabele = useFormVariable({ blockForm: form, rootCollection: collectionName });
const compile = useCompile();
const result = useMemo(() => [formVariabele].filter(Boolean), [formVariabele]);
const scope = compile(result);
const fieldSchema = useFieldSchema();
useEffect(() => {
onChange(fieldSchema.default);
}, []);
return (
<Variable.Input
value={value}
onChange={(v) => onChange(v)}
scope={scope}
style={{ minWidth: '400px', marginRight: 15 }}
className={css`
.ant-input {
width: 100% !important;
}
`}
/>
);
},
{ displayName: 'ChildDynamicComponent' },
);

View File

@ -1,8 +1,8 @@
import { observer, useForm } from '@formily/react'; import { observer, useForm } from '@formily/react';
import React from 'react';
import { action } from '@formily/reactive'; import { action } from '@formily/reactive';
import { SchemaComponent, useCompile } from '../../schema-component'; import React from 'react';
import { useCollectionManager } from '../../collection-manager'; import { useCollectionManager } from '../../collection-manager';
import { SchemaComponent, useCompile } from '../../schema-component';
export const EnableChildCollections = observer( export const EnableChildCollections = observer(
(props: any) => { (props: any) => {
@ -16,6 +16,7 @@ export const EnableChildCollections = observer(
const useAsyncDataSource = (service: any) => { const useAsyncDataSource = (service: any) => {
return (field: any, options?: any) => { return (field: any, options?: any) => {
field.loading = true; field.loading = true;
// eslint-disable-next-line promise/catch-or-return
service(field, options).then( service(field, options).then(
action.bound((data: any) => { action.bound((data: any) => {
field.dataSource = data; field.dataSource = data;
@ -28,7 +29,7 @@ export const EnableChildCollections = observer(
}; };
}; };
const loadData = async (field) => { const loadData = async (field) => {
const { childrenCollections: childCollections } = form.values?.enableChildren; const { childrenCollections: childCollections } = form.values?.enableChildren || {};
return childrenCollections return childrenCollections
.filter((v) => { .filter((v) => {
return !childCollections.find((k) => k.collection === v.name) || field.initialValue || v.name === field.value; return !childCollections.find((k) => k.collection === v.name) || field.initialValue || v.name === field.value;

View File

@ -3,10 +3,10 @@ import { observer, useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { SchemaComponent } from '../../schema-component'; import { SchemaComponent } from '../../schema-component';
import { FilterContext } from '../../schema-component/antd/filter/context'; import { FilterContext } from '../../schema-component/antd/filter/context';
import { EnableLinkage } from './components/EnableLinkage';
import { ArrayCollapse } from './components/LinkageHeader';
import { FilterDynamicComponent } from './FilterDynamicComponent'; import { FilterDynamicComponent } from './FilterDynamicComponent';
import { LinkageRuleActionGroup } from './LinkageRuleActionGroup'; import { LinkageRuleActionGroup } from './LinkageRuleActionGroup';
import { ArrayCollapse } from './components/LinkageHeader';
import { EnableLinkage } from './components/EnableLinkage';
export const FormLinkageRules = observer( export const FormLinkageRules = observer(
(props: any) => { (props: any) => {
@ -32,7 +32,7 @@ export const FormLinkageRules = observer(
type: 'object', type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel', 'x-component': 'ArrayCollapse.CollapsePanel',
'x-component-props': { 'x-component-props': {
extra: [<EnableLinkage />], extra: <EnableLinkage />,
}, },
properties: { properties: {
layout: { layout: {
@ -53,6 +53,7 @@ export const FormLinkageRules = observer(
condition: { condition: {
'x-component': 'Filter', 'x-component': 'Filter',
'x-component-props': { 'x-component-props': {
collectionName,
useProps() { useProps() {
return { return {
options, options,

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { ArrayCollapse, ArrayItems, FormDialog, FormItem, FormLayout, Input } from '@formily/antd'; import { ArrayCollapse, ArrayItems, FormDialog, FormItem, FormLayout, Input } from '@formily/antd';
import { Field, GeneralField, createForm } from '@formily/core'; import { createForm, Field, GeneralField } from '@formily/core';
import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react'; import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { error } from '@nocobase/utils/client'; import { error } from '@nocobase/utils/client';
@ -21,30 +21,31 @@ import {
import classNames from 'classnames'; import classNames from 'classnames';
import _, { cloneDeep } from 'lodash'; import _, { cloneDeep } from 'lodash';
import React, { import React, {
ReactNode,
createContext, createContext,
ReactNode,
useCallback, useCallback,
useContext, useContext,
useMemo, useMemo,
useState,
// @ts-ignore // @ts-ignore
useTransition as useReactTransition, useTransition as useReactTransition,
useState,
} from 'react'; } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
APIClientProvider,
ActionContextProvider, ActionContextProvider,
APIClientProvider,
CollectionFieldOptions, CollectionFieldOptions,
CollectionManagerContext, CollectionManagerContext,
CollectionProvider,
createDesignable,
Designable, Designable,
findFormBlock,
FormProvider, FormProvider,
RemoteSchemaComponent, RemoteSchemaComponent,
SchemaComponent, SchemaComponent,
SchemaComponentContext, SchemaComponentContext,
SchemaComponentOptions, SchemaComponentOptions,
createDesignable,
findFormBlock,
useAPIClient, useAPIClient,
useCollection, useCollection,
useCollectionManager, useCollectionManager,
@ -61,6 +62,7 @@ import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
import { FormDataTemplates } from './DataTemplates'; import { FormDataTemplates } from './DataTemplates';
import { EnableChildCollections } from './EnableChildCollections'; import { EnableChildCollections } from './EnableChildCollections';
import { ChildDynamicComponent } from './EnableChildCollections/DynamicComponent';
import { FormLinkageRules } from './LinkageRules'; import { FormLinkageRules } from './LinkageRules';
import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks'; import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks';
@ -870,6 +872,7 @@ SchemaSettings.ModalItem = function ModalItem(props) {
} = props; } = props;
const options = useContext(SchemaOptionsContext); const options = useContext(SchemaOptionsContext);
const cm = useContext(CollectionManagerContext); const cm = useContext(CollectionManagerContext);
const collection = useCollection();
const apiClient = useAPIClient(); const apiClient = useAPIClient();
if (hidden) { if (hidden) {
return null; return null;
@ -882,6 +885,7 @@ SchemaSettings.ModalItem = function ModalItem(props) {
FormDialog({ title: schema.title || title, width }, () => { FormDialog({ title: schema.title || title, width }, () => {
return ( return (
<CollectionManagerContext.Provider value={cm}> <CollectionManagerContext.Provider value={cm}>
<CollectionProvider collection={collection}>
<SchemaComponentOptions scope={options.scope} components={options.components}> <SchemaComponentOptions scope={options.scope} components={options.components}>
<FormLayout layout={'vertical'} style={{ minWidth: 520 }}> <FormLayout layout={'vertical'} style={{ minWidth: 520 }}>
<APIClientProvider apiClient={apiClient}> <APIClientProvider apiClient={apiClient}>
@ -889,6 +893,7 @@ SchemaSettings.ModalItem = function ModalItem(props) {
</APIClientProvider> </APIClientProvider>
</FormLayout> </FormLayout>
</SchemaComponentOptions> </SchemaComponentOptions>
</CollectionProvider>
</CollectionManagerContext.Provider> </CollectionManagerContext.Provider>
); );
}) })
@ -1169,13 +1174,19 @@ SchemaSettings.DataTemplates = function DataTemplates(props) {
SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(props) { SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(props) {
const { collectionName } = props; const { collectionName } = props;
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const field = useField();
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current']; const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current'];
const form = useForm();
const { getCollectionJoinField } = useCollectionManager();
const collectionField = getCollectionJoinField(fieldSchema?.parent?.['x-collection-field']) || {};
const isAssocationAdd = fieldSchema?.parent?.['x-component'] === 'CollectionField';
return ( return (
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Enable child collections')} title={t('Enable child collections')}
components={{ ArrayItems, FormLayout }} components={{ ArrayItems, FormLayout }}
scope={{ isAssocationAdd }}
schema={ schema={
{ {
type: 'object', type: 'object',
@ -1199,6 +1210,18 @@ SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(prop
'x-component': 'Checkbox', 'x-component': 'Checkbox',
default: allowAddToCurrent === undefined ? true : allowAddToCurrent, default: allowAddToCurrent === undefined ? true : allowAddToCurrent,
}, },
linkageFromForm: {
type: 'string',
title: "{{t('Linkage form form')}}",
'x-visible': '{{isAssocationAdd}}',
'x-decorator': 'FormItem',
'x-component': ChildDynamicComponent,
'x-component-props': {
collectionName: collectionField?.collectionName || collectionName,
form,
},
default: fieldSchema?.['x-component-props']?.['linkageFromForm'],
},
}, },
} as ISchema } as ISchema
} }
@ -1216,13 +1239,16 @@ SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(prop
fieldSchema['x-component-props'] = { fieldSchema['x-component-props'] = {
...fieldSchema['x-component-props'], ...fieldSchema['x-component-props'],
component: 'CreateRecordAction', component: 'CreateRecordAction',
linkageFromForm: v?.linkageFromForm,
}; };
schema['x-enable-children'] = enableChildren; schema['x-enable-children'] = enableChildren;
schema['x-allow-add-to-current'] = v.allowAddToCurrent; schema['x-allow-add-to-current'] = v.allowAddToCurrent;
schema['x-component-props'] = { schema['x-component-props'] = {
...fieldSchema['x-component-props'], ...fieldSchema['x-component-props'],
component: 'CreateRecordAction', component: 'CreateRecordAction',
linkageFromForm: v?.linkageFromForm,
}; };
field.componentProps['linkageFromForm'] = v.linkageFromForm;
dn.emit('patch', { dn.emit('patch', {
schema, schema,
}); });

View File

@ -1,6 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useCompile, useGetFilterOptions } from '../../../schema-component'; import { useCompile, useGetFilterOptions } from '../../../schema-component';
import { Schema } from '@formily/react';
import { FieldOption, Option } from '../type'; import { FieldOption, Option } from '../type';
interface GetOptionsParams { interface GetOptionsParams {
@ -50,13 +49,11 @@ export const useFormVariable = ({
blockForm, blockForm,
rootCollection, rootCollection,
operator, operator,
schema,
level, level,
}: { }: {
blockForm?: any; blockForm?: any;
rootCollection: string; rootCollection: string;
operator?: any; operator?: any;
schema: Schema;
level?: number; level?: number;
}) => { }) => {
const compile = useCompile(); const compile = useCompile();
@ -97,7 +94,6 @@ export const useFormVariable = ({
}, 5); }, 5);
}); });
}; };
const result = useMemo(() => { const result = useMemo(() => {
return ( return (
blockForm && { blockForm && {

View File

@ -1,15 +1,15 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useValues } from '../../../schema-component/antd/filter/useValues'; import { useValues } from '../../../schema-component/antd/filter/useValues';
import { useDateVariable } from './useDateVariable'; import { useDateVariable } from './useDateVariable';
import { useUserVariable } from './useUserVariable';
import { useFormVariable } from './useFormVariable'; import { useFormVariable } from './useFormVariable';
import { useIterationVariable } from './useIterationVariable'; import { useIterationVariable } from './useIterationVariable';
import { useUserVariable } from './useUserVariable';
export const useVariableOptions = ({ form, collectionField, rootCollection }) => { export const useVariableOptions = ({ form, collectionField, rootCollection }: any) => {
const { operator, schema } = useValues(); const { operator, schema } = useValues();
const userVariable = useUserVariable({ maxDepth: 3, schema }); const userVariable = useUserVariable({ maxDepth: 3, schema });
const dateVariable = useDateVariable({ operator, schema }); const dateVariable = useDateVariable({ operator, schema });
const formVariabele = useFormVariable({ blockForm: form, rootCollection, schema }); const formVariabele = useFormVariable({ blockForm: form, rootCollection });
const iterationVariabele = useIterationVariable({ blockForm: form, collectionField, schema, rootCollection }); const iterationVariabele = useIterationVariable({ blockForm: form, collectionField, schema, rootCollection });
const result = useMemo( const result = useMemo(

View File

@ -1,7 +1,7 @@
import { BelongsToManyRepository } from '@nocobase/database';
import Database from '../../database'; import Database from '../../database';
import { InheritedCollection } from '../../inherited-collection'; import { InheritedCollection } from '../../inherited-collection';
import { mockDatabase } from '../index'; import { mockDatabase } from '../index';
import { BelongsToManyRepository } from '@nocobase/database';
import pgOnly from './helper'; import pgOnly from './helper';
pgOnly()('collection inherits', () => { pgOnly()('collection inherits', () => {
@ -182,9 +182,17 @@ pgOnly()('collection inherits', () => {
}); });
it('should list data filtered by child type', async () => { it('should list data filtered by child type', async () => {
const assocs = db.collection({
name: 'assocs',
fields: [{ name: 'name', type: 'string' }],
});
const rootCollection = db.collection({ const rootCollection = db.collection({
name: 'root', name: 'root',
fields: [{ name: 'name', type: 'string' }], fields: [
{ name: 'name', type: 'string' },
{ name: 'assocs', type: 'hasMany', target: 'assocs' },
],
}); });
const child1Collection = db.collection({ const child1Collection = db.collection({
@ -202,11 +210,29 @@ pgOnly()('collection inherits', () => {
await rootCollection.repository.create({ await rootCollection.repository.create({
values: { values: {
name: 'root1', name: 'root1',
assocs: [
{
name: 'assoc1',
},
],
}, },
}); });
await child1Collection.repository.create({ await child1Collection.repository.create({
values: [{ name: 'child1-1' }, { name: 'child1-2' }], values: [
{
name: 'child1-1',
assocs: [
{
name: 'child-assoc1-1',
},
],
},
{
name: 'child1-2',
assocs: [{ name: 'child-assoc1-2' }],
},
],
}); });
await child2Collection.repository.create({ await child2Collection.repository.create({
@ -215,18 +241,38 @@ pgOnly()('collection inherits', () => {
const records = await rootCollection.repository.find({ const records = await rootCollection.repository.find({
filter: { filter: {
'tableoid.$childIn': [child1Collection.name], '__collection.$childIn': [child1Collection.name],
}, },
appends: ['assocs'],
}); });
expect(records.every((r) => r.get('__collection') === child1Collection.name)).toBe(true); expect(records.every((r) => r.get('__collection') === child1Collection.name)).toBe(true);
const records2 = await rootCollection.repository.find({ const records2 = await rootCollection.repository.find({
filter: { filter: {
'tableoid.$childNotIn': [child1Collection.name], '__collection.$childNotIn': [child1Collection.name],
}, },
}); });
expect(records2.every((r) => r.get('__collection') !== child1Collection.name)).toBe(true); expect(records2.every((r) => r.get('__collection') !== child1Collection.name)).toBe(true);
const recordsWithFilter = await rootCollection.repository.find({
filter: {
'__collection.$childIn': [child1Collection.name],
assocs: {
name: 'child-assoc1-1',
},
},
});
expect(recordsWithFilter.every((r) => r.get('__collection') == child1Collection.name)).toBe(true);
const filterWithUndefined = await rootCollection.repository.find({
filter: {
'__collection.$childIn': 'undefined',
},
});
expect(filterWithUndefined).toHaveLength(0);
}); });
it('should list collection name in relation repository', async () => { it('should list collection name in relation repository', async () => {

View File

@ -9,7 +9,7 @@ export class UidField extends Field {
init() { init() {
const { name, prefix = '', pattern } = this.options; const { name, prefix = '', pattern } = this.options;
const re = new RegExp(pattern || '^[A-Za-z0-9][A-Za-z0-9_-]*$'); const re = new RegExp(pattern || '^[A-Za-z0-9_][A-Za-z0-9_-]*$');
this.listener = async (instance) => { this.listener = async (instance) => {
const value = instance.get(name); const value = instance.get(name);
if (!value) { if (!value) {

View File

@ -226,6 +226,7 @@ export default class FilterParser {
if (values && typeof values === 'object' && value && typeof value === 'object') { if (values && typeof values === 'object' && value && typeof value === 'object') {
value = { ...value, ...values }; value = { ...value, ...values };
} }
_.set(where, paths, value); _.set(where, paths, value);
} }

View File

@ -1,4 +1,5 @@
import { Op, Sequelize } from 'sequelize'; import lodash from 'lodash';
import { Sequelize } from 'sequelize';
const mapVal = (values, db) => const mapVal = (values, db) =>
values.map((v) => { values.map((v) => {
@ -6,19 +7,37 @@ const mapVal = (values, db) =>
return Sequelize.literal(`'${collection.tableNameAsString()}'::regclass`); return Sequelize.literal(`'${collection.tableNameAsString()}'::regclass`);
}); });
const filterItems = (values, db) => {
return lodash
.castArray(values)
.map((v) => {
const collection = db.getCollection(v);
if (!collection) return null;
return `'${collection.tableNameAsString()}'::regclass`;
})
.filter(Boolean);
};
const joinValues = (items) => items.join(', ');
export default { export default {
$childIn(values, ctx: any) { $childIn(values, ctx: any) {
const db = ctx.db; const db = ctx.db;
const items = filterItems(values, db);
return { if (items.length) {
[Op.in]: mapVal(values, db), return Sequelize.literal(`"${ctx.model.name}"."tableoid" IN (${joinValues(items)})`);
}; } else {
return Sequelize.literal(`1 = 2`);
}
}, },
$childNotIn(values, ctx: any) { $childNotIn(values, ctx: any) {
const db = ctx.db; const db = ctx.db;
const items = filterItems(values, db);
return { if (items.length) {
[Op.notIn]: mapVal(values, db), return Sequelize.literal(`"${ctx.model.name}"."tableoid" NOT IN (${joinValues(items)})`);
}; } else {
return Sequelize.literal(`1 = 1`);
}
}, },
} as Record<string, any>; } as Record<string, any>;

View File

@ -1,3 +1,4 @@
import { flatten } from 'flat';
import lodash from 'lodash'; import lodash from 'lodash';
import { import {
Association, Association,
@ -18,6 +19,7 @@ import { Collection } from './collection';
import { Database } from './database'; import { Database } from './database';
import mustHaveFilter from './decorators/must-have-filter-decorator'; import mustHaveFilter from './decorators/must-have-filter-decorator';
import { transactionWrapperBuilder } from './decorators/transaction-decorator'; import { transactionWrapperBuilder } from './decorators/transaction-decorator';
import { EagerLoadingTree } from './eager-loading/eager-loading-tree';
import { ArrayFieldRepository } from './field-repository/array-field-repository'; import { ArrayFieldRepository } from './field-repository/array-field-repository';
import { ArrayField, RelationField } from './fields'; import { ArrayField, RelationField } from './fields';
import FilterParser from './filter-parser'; import FilterParser from './filter-parser';
@ -31,8 +33,6 @@ import { HasOneRepository } from './relation-repository/hasone-repository';
import { RelationRepository } from './relation-repository/relation-repository'; import { RelationRepository } from './relation-repository/relation-repository';
import { updateAssociations, updateModelByValues } from './update-associations'; import { updateAssociations, updateModelByValues } from './update-associations';
import { UpdateGuard } from './update-guard'; import { UpdateGuard } from './update-guard';
import { EagerLoadingTree } from './eager-loading/eager-loading-tree';
import { flatten } from 'flat';
const debug = require('debug')('noco-database'); const debug = require('debug')('noco-database');

View File

@ -0,0 +1,40 @@
import { Migration } from '@nocobase/server';
import _ from 'lodash';
export default class extends Migration {
async up() {
if (!this.db.inDialect('postgres')) {
return;
}
const repository = this.db.getRepository('collections');
let names = [];
const items = await repository.find();
for (const item of items) {
if (Array.isArray(item.options?.inherits) && item.options.inherits.length) {
names.push(item.name);
names.push(...item.options.inherits);
}
}
names = _.uniq(names);
console.log('collection names:', names);
for (const name of names) {
const fieldRepository = this.db.getRepository('fields');
await fieldRepository.firstOrCreate({
values: {
collectionName: name,
name: '__collection',
type: 'virtual',
interface: 'tableoid',
uiSchema: {
type: 'string',
title: '{{t("Table OID")}}',
'x-component': 'CollectionSelect',
'x-component-props': { isTableOid: true },
'x-read-pretty': true,
},
},
filterKeys: ['name', 'collectionName'],
});
}
}
}

View File

@ -14,7 +14,7 @@ const useCreateCollectionField = (record) => {
}; };
}; };
export const AddFieldAction = ({ item: record }) => { export const AddFieldAction = ({ item: record, database }) => {
return ( return (
<AddCollectionFieldAction <AddCollectionFieldAction
trigger={['click']} trigger={['click']}
@ -24,6 +24,7 @@ export const AddFieldAction = ({ item: record }) => {
}, },
}} }}
item={record} item={record}
database={database}
scope={{ scope={{
useCancelAction, useCancelAction,
useCreateCollectionField: () => useCreateCollectionField(record), useCreateCollectionField: () => useCreateCollectionField(record),

View File

@ -5,6 +5,7 @@ import { uid } from '@formily/shared';
import { import {
Action, Action,
Checkbox, Checkbox,
collection,
CollectionCategroriesContext, CollectionCategroriesContext,
CollectionField, CollectionField,
CollectionProvider, CollectionProvider,
@ -19,7 +20,6 @@ import {
SchemaComponent, SchemaComponent,
SchemaComponentProvider, SchemaComponentProvider,
Select, Select,
collection,
useCollectionManager, useCollectionManager,
useCompile, useCompile,
useCurrentAppInfo, useCurrentAppInfo,
@ -190,6 +190,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
const [collapse, setCollapse] = useState(false); const [collapse, setCollapse] = useState(false);
const { t } = useGCMTranslation(); const { t } = useGCMTranslation();
const compile = useCompile(); const compile = useCompile();
const database = useCurrentAppInfo();
const portsData = groupBy(ports.items, (v) => { const portsData = groupBy(ports.items, (v) => {
if ( if (
v.isForeignKey || v.isForeignKey ||
@ -292,6 +293,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
...property, ...property,
title, title,
}, },
database,
}, },
}, },
update: { update: {