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:
parent
637ccb0457
commit
046a0b4f4d
@ -9,10 +9,10 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useRequest } from '../../api-client';
|
||||
import { RecordProvider, useRecord } from '../../record-provider';
|
||||
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
|
||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||
import { useCancelAction } from '../action-hooks';
|
||||
import { useCollectionManager } from '../hooks';
|
||||
import { IField } from '../interfaces/types';
|
||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||
import * as components from './components';
|
||||
import { getOptions } from './interfaces';
|
||||
|
||||
@ -167,13 +167,21 @@ export const AddCollectionField = (props) => {
|
||||
};
|
||||
|
||||
export const AddFieldAction = (props) => {
|
||||
const { scope, getContainer, item: record, children, trigger, align } = props;
|
||||
const { getInterface, getTemplate } = useCollectionManager();
|
||||
const { scope, getContainer, item: record, children, trigger, align, database } = props;
|
||||
const { getInterface, getTemplate, collections } = useCollectionManager();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [targetScope, setTargetScope] = useState();
|
||||
const [schema, setSchema] = useState({});
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const currentCollections = useMemo(() => {
|
||||
return collections.map((v) => {
|
||||
return {
|
||||
label: compile(v.title),
|
||||
value: v.name,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
const getFieldOptions = useCallback(() => {
|
||||
const { availableFieldInterfaces } = getTemplate(record.template) || {};
|
||||
const { exclude, include } = availableFieldInterfaces || {};
|
||||
@ -185,6 +193,8 @@ export const AddFieldAction = (props) => {
|
||||
children: v.children.filter((v) => {
|
||||
if (v.value === 'id') {
|
||||
return typeof record['autoGenId'] === 'boolean' ? record['autoGenId'] : true;
|
||||
} else if (v.value === 'tableoid') {
|
||||
return database?.dialect === 'postgres';
|
||||
} else {
|
||||
return typeof record[v.value] === 'boolean' ? record[v.value] : true;
|
||||
}
|
||||
@ -241,7 +251,6 @@ export const AddFieldAction = (props) => {
|
||||
};
|
||||
});
|
||||
}, [getFieldOptions]);
|
||||
|
||||
const menu = useMemo<MenuProps>(() => {
|
||||
return {
|
||||
style: {
|
||||
@ -286,6 +295,7 @@ export const AddFieldAction = (props) => {
|
||||
record,
|
||||
showReverseFieldConfig: true,
|
||||
targetScope,
|
||||
collections: currentCollections,
|
||||
...scope,
|
||||
}}
|
||||
/>
|
||||
|
@ -1,29 +1,30 @@
|
||||
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 { Space, Switch, Table, TableColumnProps, Tag, Tooltip } from 'antd';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCurrentAppInfo } from '../../appInfo';
|
||||
import { RecordProvider, useRecord } from '../../record-provider';
|
||||
import { Action, useAttach, useCompile } from '../../schema-component';
|
||||
import {
|
||||
ResourceActionContext,
|
||||
ResourceActionProvider,
|
||||
useResourceActionContext,
|
||||
useResourceContext,
|
||||
} from '../ResourceActionProvider';
|
||||
import {
|
||||
isDeleteButtonDisabled,
|
||||
useBulkDestroyActionAndRefreshCM,
|
||||
useDestroyActionAndRefreshCM,
|
||||
} from '../action-hooks';
|
||||
import { useCollectionManager } from '../hooks/useCollectionManager';
|
||||
import {
|
||||
ResourceActionContext,
|
||||
ResourceActionProvider,
|
||||
useResourceActionContext,
|
||||
useResourceContext,
|
||||
} from '../ResourceActionProvider';
|
||||
import { AddCollectionField } from './AddFieldAction';
|
||||
import { EditCollectionField } from './EditFieldAction';
|
||||
import { OverridingCollectionField } from './OverridingCollectionField';
|
||||
import { collection } from './schemas/collectionFields';
|
||||
import { SyncFieldsAction } from './SyncFieldsAction';
|
||||
import { ViewCollectionField } from './ViewInheritedField';
|
||||
import { collection } from './schemas/collectionFields';
|
||||
|
||||
const indentStyle = css`
|
||||
.ant-table {
|
||||
@ -269,6 +270,9 @@ export const CollectionFields = () => {
|
||||
const compile = useCompile();
|
||||
const field = useField<Field>();
|
||||
const { name } = useRecord();
|
||||
const {
|
||||
data: { database },
|
||||
} = useCurrentAppInfo();
|
||||
const { getInterface, getInheritCollections, getCollection, getCurrentCollectionFields } = useCollectionManager();
|
||||
const form = useMemo(() => createForm(), []);
|
||||
const f = useAttach(form.createArrayField({ ...field.props, basePath: '' }));
|
||||
@ -403,7 +407,7 @@ export const CollectionFields = () => {
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
const addProps = { type: 'primary' };
|
||||
const addProps = { type: 'primary', database };
|
||||
const syncProps = { type: 'primary' };
|
||||
return (
|
||||
<ResourceActionProvider {...resourceActionProps}>
|
||||
|
@ -3,15 +3,15 @@ import { ISchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient, useRequest } from '../../api-client';
|
||||
import { RecordProvider, useRecord } from '../../record-provider';
|
||||
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
|
||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||
import { useCancelAction, useUpdateAction } from '../action-hooks';
|
||||
import { useCollectionManager } from '../hooks';
|
||||
import { IField } from '../interfaces/types';
|
||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||
import * as components from './components';
|
||||
|
||||
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
|
||||
@ -136,13 +136,21 @@ export const EditCollectionField = (props) => {
|
||||
|
||||
export const EditFieldAction = (props) => {
|
||||
const { scope, getContainer, item: record, children } = props;
|
||||
const { getInterface } = useCollectionManager();
|
||||
const { getInterface, collections } = useCollectionManager();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [schema, setSchema] = useState({});
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
const compile = useCompile();
|
||||
const [data, setData] = useState<any>({});
|
||||
const currentCollections = useMemo(() => {
|
||||
return collections.map((v) => {
|
||||
return {
|
||||
label: compile(v.title),
|
||||
value: v.name,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<RecordProvider record={record}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
@ -184,6 +192,7 @@ export const EditFieldAction = (props) => {
|
||||
useUpdateCollectionField,
|
||||
useCancelAction,
|
||||
showReverseFieldConfig: !data?.reverseField,
|
||||
collections: currentCollections,
|
||||
...scope,
|
||||
}}
|
||||
/>
|
||||
|
@ -4,7 +4,6 @@ import omit from 'lodash/omit';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollection, useCollectionManager } from '.';
|
||||
import { useCompile } from '..';
|
||||
import { useRequest } from '../api-client';
|
||||
import { useRecord } from '../record-provider';
|
||||
import { useActionContext } from '../schema-component';
|
||||
@ -108,10 +107,28 @@ export const useChildrenCollections = (collectionName: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCollectionFilterOptions = (collectionName: string) => {
|
||||
const { getCollectionFields, getInterface, getChildrenCollections, getCollection } = useCollectionManager();
|
||||
const compile = useCompile();
|
||||
export const useSelfAndChildrenCollections = (collectionName: string) => {
|
||||
const { getChildrenCollections, getCollection } = useCollectionManager();
|
||||
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(() => {
|
||||
const fields = getCollectionFields(collectionName);
|
||||
const field2option = (field, depth) => {
|
||||
@ -161,44 +178,6 @@ export const useCollectionFilterOptions = (collectionName: string) => {
|
||||
return options;
|
||||
};
|
||||
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;
|
||||
}, [collectionName]);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
export * from './checkbox';
|
||||
export * from './checkboxGroup';
|
||||
export * from './chinaRegion';
|
||||
export * from './collection';
|
||||
export * from './createdAt';
|
||||
export * from './createdBy';
|
||||
export * from './datetime';
|
||||
@ -25,6 +26,7 @@ export * from './radioGroup';
|
||||
export * from './richText';
|
||||
export * from './select';
|
||||
export * from './subTable';
|
||||
export * from './tableoid';
|
||||
export * from './textarea';
|
||||
export * from './time';
|
||||
export * from './updatedAt';
|
||||
|
@ -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}}',
|
||||
};
|
||||
|
@ -118,3 +118,54 @@ export const boolean = [
|
||||
{ label: '{{t("Yes")}}', value: '$isTruly', selected: true, 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 },
|
||||
];
|
@ -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,
|
||||
},
|
||||
};
|
@ -705,5 +705,6 @@ export default {
|
||||
"Find by the following fields":"Find by the following fields",
|
||||
"Create":"Create",
|
||||
"Current form": "Current form",
|
||||
"Current object":"Current object"
|
||||
"Current object":"Current object",
|
||||
"Linkage form form data":"Linkage form form data",
|
||||
};
|
||||
|
@ -616,5 +616,6 @@ export default {
|
||||
"Find by the following fields":"次のフィールドで検索",
|
||||
"Create":"新規のみ" ,
|
||||
"Current form":"現在のフォーム",
|
||||
"Current object":"現在のオブジェクト"
|
||||
"Current object":"現在のオブジェクト",
|
||||
"Linkage form form data":"フォームデータから連動",
|
||||
}
|
||||
|
@ -790,4 +790,5 @@ export default {
|
||||
"File manager": "文件管理器",
|
||||
"Direct duplicate": "直接复制",
|
||||
"Copy into the form and continue to fill in": "复制到表单并继续填写",
|
||||
"Linkage form form data":"从表单数据里联动",
|
||||
}
|
||||
|
@ -63,9 +63,8 @@ const ToManyNester = observer(
|
||||
insertCount: 1,
|
||||
});
|
||||
field.value.splice(index + 1, 0, {});
|
||||
|
||||
each(field.form.fields, (field, key) => {
|
||||
if (!field) {
|
||||
each(field.form.fields, (targetField, key) => {
|
||||
if (!targetField) {
|
||||
delete field.form.fields[key];
|
||||
}
|
||||
});
|
||||
|
@ -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 React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
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 { FilterContext } from '../filter/context';
|
||||
|
||||
export type CollectionSelectProps = SelectProps<any, any> & {
|
||||
filter?: (item: any, index: number, array: any[]) => boolean;
|
||||
isTableOid?: boolean;
|
||||
};
|
||||
|
||||
function useOptions({ filter }: CollectionSelectProps) {
|
||||
function useOptions({ filter, isTableOid }: CollectionSelectProps) {
|
||||
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 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
|
||||
.filter((item) => !item.hidden)
|
||||
.map((item) => ({
|
||||
label: compile(item.title),
|
||||
value: item.name,
|
||||
label: compile(item.title || item.label),
|
||||
value: item.name || item.value,
|
||||
color: item.category?.color,
|
||||
}));
|
||||
}
|
||||
@ -27,7 +43,6 @@ export const CollectionSelect = connect(
|
||||
const { filter, ...others } = props;
|
||||
const options = useOptions(props);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Select
|
||||
placeholder={t('Select collection')}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { ObjectField as ObjectFieldModel } from '@formily/core';
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useRequest } from '../../../api-client';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import { DatePickerProvider } from '../date-picker';
|
||||
import { FilterContext } from './context';
|
||||
import { FilterActionDesigner } from './Filter.Action.Designer';
|
||||
import { FilterAction } from './FilterAction';
|
||||
import { FilterGroup } from './FilterGroup';
|
||||
import { SaveDefaultValue } from './SaveDefaultValue';
|
||||
import { FilterContext } from './context';
|
||||
|
||||
const useDef = (options) => {
|
||||
const field = useField<ObjectFieldModel>();
|
||||
@ -18,14 +18,17 @@ const useDef = (options) => {
|
||||
export const Filter: any = observer(
|
||||
(props: any) => {
|
||||
const { useDataSource = useDef } = props;
|
||||
const { options, dynamicComponent, className } = useProps(props);
|
||||
const { options, dynamicComponent, className, collectionName } = useProps(props);
|
||||
const field = useField<ObjectFieldModel>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const fieldSchema: any = useFieldSchema();
|
||||
useDataSource({
|
||||
onSuccess(data) {
|
||||
field.dataSource = data?.data || [];
|
||||
},
|
||||
});
|
||||
useEffect(() => {
|
||||
field.initialValue = fieldSchema.defaultValue;
|
||||
}, []);
|
||||
return (
|
||||
<div className={className}>
|
||||
<DatePickerProvider value={{ utc: false }}>
|
||||
@ -36,6 +39,7 @@ export const Filter: any = observer(
|
||||
dynamicComponent,
|
||||
options: options || field.dataSource || [],
|
||||
disabled: props.disabled,
|
||||
collectionName,
|
||||
}}
|
||||
>
|
||||
<FilterGroup {...props} bordered={false} />
|
||||
|
@ -5,8 +5,8 @@ import { Cascader, Select, Space } from 'antd';
|
||||
import React, { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile } from '../..';
|
||||
import { DynamicComponent } from './DynamicComponent';
|
||||
import { RemoveConditionContext } from './context';
|
||||
import { DynamicComponent } from './DynamicComponent';
|
||||
import { useValues } from './useValues';
|
||||
|
||||
export const FilterItem = observer(
|
||||
|
@ -8,6 +8,7 @@ export interface FilterContextProps {
|
||||
dynamicComponent?: any;
|
||||
options?: any[];
|
||||
disabled?: boolean;
|
||||
collectionName?: string;
|
||||
}
|
||||
|
||||
export const RemoveConditionContext = createContext(null);
|
||||
|
@ -2,108 +2,25 @@ import { Field } from '@formily/core';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import flat from 'flat';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile } from '../..';
|
||||
import { useBlockRequestContext } from '../../../block-provider';
|
||||
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||
|
||||
export const useGetFilterOptions = () => {
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const compile = useCompile();
|
||||
const { getChildrenCollections } = useCollectionManager();
|
||||
const collection = useCollection();
|
||||
const getFilterFieldOptions = useGetFilterFieldOptions();
|
||||
|
||||
return (collectionName) => {
|
||||
const fields = getCollectionFields(collectionName);
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
||||
export const useFilterOptions = (collectionName: string) => {
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const compile = useCompile();
|
||||
const { getChildrenCollections } = useCollectionManager();
|
||||
const collection = useCollection();
|
||||
const fields = getCollectionFields(collectionName);
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -61,7 +61,7 @@ export const useValues = () => {
|
||||
const s2 = cloneDeep(operator?.schema);
|
||||
field.data.schema = merge(s1, s2);
|
||||
field.data.dataIndex = dataIndex;
|
||||
field.data.value = operator?.noValue ? operator.default || true : null;
|
||||
field.data.value = operator?.noValue ? operator.default || true : undefined;
|
||||
data2value();
|
||||
},
|
||||
setOperator(operatorValue) {
|
||||
@ -71,7 +71,7 @@ export const useValues = () => {
|
||||
const s1 = cloneDeep(option?.schema);
|
||||
const s2 = cloneDeep(operator?.schema);
|
||||
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();
|
||||
},
|
||||
setValue(value) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
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 { ISchema, Schema, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { ISchema, observer, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { Select } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
@ -20,9 +20,9 @@ import {
|
||||
} from '../../../collection-manager';
|
||||
import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields';
|
||||
import { GeneralSchemaItems } from '../../../schema-items/GeneralSchemaItems';
|
||||
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
|
||||
import { VariableInput } from '../../../schema-settings/VariableInput/VariableInput';
|
||||
import { GeneralSchemaDesigner, isPatternDisabled, isShowDefaultValue, SchemaSettings } from '../../../schema-settings';
|
||||
import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch';
|
||||
import { VariableInput } from '../../../schema-settings/VariableInput/VariableInput';
|
||||
import { isVariable, parseVariables, useVariablesCtx } from '../../common/utils/uitls';
|
||||
import { SchemaComponent } from '../../core';
|
||||
import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks';
|
||||
@ -153,7 +153,7 @@ FormItem.Designer = function Designer() {
|
||||
readOnlyMode = 'read-pretty';
|
||||
}
|
||||
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 defaultSort = field.componentProps?.service?.params?.sort || [];
|
||||
const fieldMode = field?.componentProps?.['mode'] || (isFileField ? 'FileManager' : 'Select');
|
||||
@ -446,10 +446,11 @@ FormItem.Designer = function Designer() {
|
||||
title: t('Set the data scope'),
|
||||
properties: {
|
||||
filter: {
|
||||
default: defaultFilter,
|
||||
defaultValue: defaultFilter,
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {
|
||||
collectionName: collectionField?.target,
|
||||
dynamicComponent: (props) =>
|
||||
FilterDynamicComponent({
|
||||
...props,
|
||||
|
@ -11,7 +11,7 @@ import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { Select, defaultFieldNames } from '../select';
|
||||
import { defaultFieldNames, Select } from '../select';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
import { extractFilterfield, extractValuesByPattern, generatePattern, parseVariables } from './utils';
|
||||
const EMPTY = 'N/A';
|
||||
@ -156,11 +156,18 @@ const InternalRemoteSelect = connect(
|
||||
str = str.replace('$iteration.', `$iteration.${path.join('.')}.`);
|
||||
}
|
||||
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(
|
||||
JSON.stringify(c).replace(jsonlogic.value, str.endsWith('id') ? parseValue ?? 0 : parseValue),
|
||||
);
|
||||
results.push(filterObj);
|
||||
}
|
||||
}
|
||||
});
|
||||
return { [type]: results };
|
||||
};
|
||||
@ -173,7 +180,9 @@ const InternalRemoteSelect = connect(
|
||||
pageSize: 200,
|
||||
...service?.params,
|
||||
// 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;
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import flat from 'flat';
|
||||
import _, { every, findIndex, isArray, some } from 'lodash';
|
||||
import moment from 'moment';
|
||||
@ -8,8 +7,9 @@ import jsonLogic from '../../common/utils/logic';
|
||||
|
||||
type VariablesCtx = {
|
||||
/** 当前登录的用户 */
|
||||
$user: Record<string, any>;
|
||||
$date: Record<string, any>;
|
||||
$user?: Record<string, any>;
|
||||
$date?: Record<string, any>;
|
||||
$form?: Record<string, any>;
|
||||
};
|
||||
|
||||
export const useVariablesCtx = (): VariablesCtx => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DownOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
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 React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useDesignable } from '../../';
|
||||
@ -9,6 +9,7 @@ import { CollectionProvider, useCollection, useCollectionManager } from '../../c
|
||||
import { useRecord } from '../../record-provider';
|
||||
import { ActionContextProvider, useActionContext, useCompile } from '../../schema-component';
|
||||
import { linkageAction } from '../../schema-component/antd/action/utils';
|
||||
import { parseVariables } from '../../schema-component/common/utils/uitls';
|
||||
|
||||
export const actionDesignerCss = css`
|
||||
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 recordPkValue = useRecordPkValue();
|
||||
const collection = useCollection();
|
||||
@ -120,14 +121,20 @@ export const CreateRecordAction = observer(
|
||||
{ displayName: 'CreateRecordAction' },
|
||||
);
|
||||
|
||||
function getLinkageCollection(str, form) {
|
||||
const data = parseVariables(str, { $form: form.values });
|
||||
return data;
|
||||
}
|
||||
export const CreateAction = observer(
|
||||
(props: any) => {
|
||||
const { onClick } = props;
|
||||
const collection = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field: any = useField();
|
||||
const form = useForm();
|
||||
const enableChildren = fieldSchema['x-enable-children'] || [];
|
||||
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 { getChildrenCollections } = useCollectionManager();
|
||||
const totalChildCollections = getChildrenCollections(collection.name);
|
||||
@ -182,6 +189,7 @@ export const CreateAction = observer(
|
||||
return (
|
||||
<div className={actionDesignerCss}>
|
||||
{inheritsCollections?.length > 0 ? (
|
||||
!linkageFromForm ? (
|
||||
allowAddToCurrent === undefined || allowAddToCurrent ? (
|
||||
<Dropdown.Button
|
||||
type={componentType}
|
||||
@ -207,6 +215,27 @@ export const CreateAction = observer(
|
||||
}
|
||||
</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
|
||||
type={componentType}
|
||||
|
@ -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' },
|
||||
);
|
@ -1,8 +1,8 @@
|
||||
import { observer, useForm } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { action } from '@formily/reactive';
|
||||
import { SchemaComponent, useCompile } from '../../schema-component';
|
||||
import React from 'react';
|
||||
import { useCollectionManager } from '../../collection-manager';
|
||||
import { SchemaComponent, useCompile } from '../../schema-component';
|
||||
|
||||
export const EnableChildCollections = observer(
|
||||
(props: any) => {
|
||||
@ -16,6 +16,7 @@ export const EnableChildCollections = observer(
|
||||
const useAsyncDataSource = (service: any) => {
|
||||
return (field: any, options?: any) => {
|
||||
field.loading = true;
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
service(field, options).then(
|
||||
action.bound((data: any) => {
|
||||
field.dataSource = data;
|
||||
@ -28,7 +29,7 @@ export const EnableChildCollections = observer(
|
||||
};
|
||||
};
|
||||
const loadData = async (field) => {
|
||||
const { childrenCollections: childCollections } = form.values?.enableChildren;
|
||||
const { childrenCollections: childCollections } = form.values?.enableChildren || {};
|
||||
return childrenCollections
|
||||
.filter((v) => {
|
||||
return !childCollections.find((k) => k.collection === v.name) || field.initialValue || v.name === field.value;
|
||||
|
@ -3,10 +3,10 @@ import { observer, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { SchemaComponent } from '../../schema-component';
|
||||
import { FilterContext } from '../../schema-component/antd/filter/context';
|
||||
import { EnableLinkage } from './components/EnableLinkage';
|
||||
import { ArrayCollapse } from './components/LinkageHeader';
|
||||
import { FilterDynamicComponent } from './FilterDynamicComponent';
|
||||
import { LinkageRuleActionGroup } from './LinkageRuleActionGroup';
|
||||
import { ArrayCollapse } from './components/LinkageHeader';
|
||||
import { EnableLinkage } from './components/EnableLinkage';
|
||||
|
||||
export const FormLinkageRules = observer(
|
||||
(props: any) => {
|
||||
@ -32,7 +32,7 @@ export const FormLinkageRules = observer(
|
||||
type: 'object',
|
||||
'x-component': 'ArrayCollapse.CollapsePanel',
|
||||
'x-component-props': {
|
||||
extra: [<EnableLinkage />],
|
||||
extra: <EnableLinkage />,
|
||||
},
|
||||
properties: {
|
||||
layout: {
|
||||
@ -53,6 +53,7 @@ export const FormLinkageRules = observer(
|
||||
condition: {
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {
|
||||
collectionName,
|
||||
useProps() {
|
||||
return {
|
||||
options,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
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 { uid } from '@formily/shared';
|
||||
import { error } from '@nocobase/utils/client';
|
||||
@ -21,30 +21,31 @@ import {
|
||||
import classNames from 'classnames';
|
||||
import _, { cloneDeep } from 'lodash';
|
||||
import React, {
|
||||
ReactNode,
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
// @ts-ignore
|
||||
useTransition as useReactTransition,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
APIClientProvider,
|
||||
ActionContextProvider,
|
||||
APIClientProvider,
|
||||
CollectionFieldOptions,
|
||||
CollectionManagerContext,
|
||||
CollectionProvider,
|
||||
createDesignable,
|
||||
Designable,
|
||||
findFormBlock,
|
||||
FormProvider,
|
||||
RemoteSchemaComponent,
|
||||
SchemaComponent,
|
||||
SchemaComponentContext,
|
||||
SchemaComponentOptions,
|
||||
createDesignable,
|
||||
findFormBlock,
|
||||
useAPIClient,
|
||||
useCollection,
|
||||
useCollectionManager,
|
||||
@ -61,6 +62,7 @@ import { useSchemaTemplateManager } from '../schema-templates';
|
||||
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
|
||||
import { FormDataTemplates } from './DataTemplates';
|
||||
import { EnableChildCollections } from './EnableChildCollections';
|
||||
import { ChildDynamicComponent } from './EnableChildCollections/DynamicComponent';
|
||||
import { FormLinkageRules } from './LinkageRules';
|
||||
import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks';
|
||||
|
||||
@ -870,6 +872,7 @@ SchemaSettings.ModalItem = function ModalItem(props) {
|
||||
} = props;
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const cm = useContext(CollectionManagerContext);
|
||||
const collection = useCollection();
|
||||
const apiClient = useAPIClient();
|
||||
if (hidden) {
|
||||
return null;
|
||||
@ -882,6 +885,7 @@ SchemaSettings.ModalItem = function ModalItem(props) {
|
||||
FormDialog({ title: schema.title || title, width }, () => {
|
||||
return (
|
||||
<CollectionManagerContext.Provider value={cm}>
|
||||
<CollectionProvider collection={collection}>
|
||||
<SchemaComponentOptions scope={options.scope} components={options.components}>
|
||||
<FormLayout layout={'vertical'} style={{ minWidth: 520 }}>
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
@ -889,6 +893,7 @@ SchemaSettings.ModalItem = function ModalItem(props) {
|
||||
</APIClientProvider>
|
||||
</FormLayout>
|
||||
</SchemaComponentOptions>
|
||||
</CollectionProvider>
|
||||
</CollectionManagerContext.Provider>
|
||||
);
|
||||
})
|
||||
@ -1169,13 +1174,19 @@ SchemaSettings.DataTemplates = function DataTemplates(props) {
|
||||
SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(props) {
|
||||
const { collectionName } = props;
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField();
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
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 (
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Enable child collections')}
|
||||
components={{ ArrayItems, FormLayout }}
|
||||
scope={{ isAssocationAdd }}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
@ -1199,6 +1210,18 @@ SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(prop
|
||||
'x-component': 'Checkbox',
|
||||
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
|
||||
}
|
||||
@ -1216,13 +1239,16 @@ SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(prop
|
||||
fieldSchema['x-component-props'] = {
|
||||
...fieldSchema['x-component-props'],
|
||||
component: 'CreateRecordAction',
|
||||
linkageFromForm: v?.linkageFromForm,
|
||||
};
|
||||
schema['x-enable-children'] = enableChildren;
|
||||
schema['x-allow-add-to-current'] = v.allowAddToCurrent;
|
||||
schema['x-component-props'] = {
|
||||
...fieldSchema['x-component-props'],
|
||||
component: 'CreateRecordAction',
|
||||
linkageFromForm: v?.linkageFromForm,
|
||||
};
|
||||
field.componentProps['linkageFromForm'] = v.linkageFromForm;
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useCompile, useGetFilterOptions } from '../../../schema-component';
|
||||
import { Schema } from '@formily/react';
|
||||
import { FieldOption, Option } from '../type';
|
||||
|
||||
interface GetOptionsParams {
|
||||
@ -50,13 +49,11 @@ export const useFormVariable = ({
|
||||
blockForm,
|
||||
rootCollection,
|
||||
operator,
|
||||
schema,
|
||||
level,
|
||||
}: {
|
||||
blockForm?: any;
|
||||
rootCollection: string;
|
||||
operator?: any;
|
||||
schema: Schema;
|
||||
level?: number;
|
||||
}) => {
|
||||
const compile = useCompile();
|
||||
@ -97,7 +94,6 @@ export const useFormVariable = ({
|
||||
}, 5);
|
||||
});
|
||||
};
|
||||
|
||||
const result = useMemo(() => {
|
||||
return (
|
||||
blockForm && {
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useValues } from '../../../schema-component/antd/filter/useValues';
|
||||
import { useDateVariable } from './useDateVariable';
|
||||
import { useUserVariable } from './useUserVariable';
|
||||
import { useFormVariable } from './useFormVariable';
|
||||
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 userVariable = useUserVariable({ maxDepth: 3, 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 result = useMemo(
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { BelongsToManyRepository } from '@nocobase/database';
|
||||
import Database from '../../database';
|
||||
import { InheritedCollection } from '../../inherited-collection';
|
||||
import { mockDatabase } from '../index';
|
||||
import { BelongsToManyRepository } from '@nocobase/database';
|
||||
import pgOnly from './helper';
|
||||
|
||||
pgOnly()('collection inherits', () => {
|
||||
@ -182,9 +182,17 @@ pgOnly()('collection inherits', () => {
|
||||
});
|
||||
|
||||
it('should list data filtered by child type', async () => {
|
||||
const assocs = db.collection({
|
||||
name: 'assocs',
|
||||
fields: [{ name: 'name', type: 'string' }],
|
||||
});
|
||||
|
||||
const rootCollection = db.collection({
|
||||
name: 'root',
|
||||
fields: [{ name: 'name', type: 'string' }],
|
||||
fields: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'assocs', type: 'hasMany', target: 'assocs' },
|
||||
],
|
||||
});
|
||||
|
||||
const child1Collection = db.collection({
|
||||
@ -202,11 +210,29 @@ pgOnly()('collection inherits', () => {
|
||||
await rootCollection.repository.create({
|
||||
values: {
|
||||
name: 'root1',
|
||||
assocs: [
|
||||
{
|
||||
name: 'assoc1',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
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({
|
||||
@ -215,18 +241,38 @@ pgOnly()('collection inherits', () => {
|
||||
|
||||
const records = await rootCollection.repository.find({
|
||||
filter: {
|
||||
'tableoid.$childIn': [child1Collection.name],
|
||||
'__collection.$childIn': [child1Collection.name],
|
||||
},
|
||||
appends: ['assocs'],
|
||||
});
|
||||
|
||||
expect(records.every((r) => r.get('__collection') === child1Collection.name)).toBe(true);
|
||||
|
||||
const records2 = await rootCollection.repository.find({
|
||||
filter: {
|
||||
'tableoid.$childNotIn': [child1Collection.name],
|
||||
'__collection.$childNotIn': [child1Collection.name],
|
||||
},
|
||||
});
|
||||
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 () => {
|
||||
|
@ -9,7 +9,7 @@ export class UidField extends Field {
|
||||
|
||||
init() {
|
||||
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) => {
|
||||
const value = instance.get(name);
|
||||
if (!value) {
|
||||
|
@ -226,6 +226,7 @@ export default class FilterParser {
|
||||
if (values && typeof values === 'object' && value && typeof value === 'object') {
|
||||
value = { ...value, ...values };
|
||||
}
|
||||
|
||||
_.set(where, paths, value);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Op, Sequelize } from 'sequelize';
|
||||
import lodash from 'lodash';
|
||||
import { Sequelize } from 'sequelize';
|
||||
|
||||
const mapVal = (values, db) =>
|
||||
values.map((v) => {
|
||||
@ -6,19 +7,37 @@ const mapVal = (values, db) =>
|
||||
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 {
|
||||
$childIn(values, ctx: any) {
|
||||
const db = ctx.db;
|
||||
const items = filterItems(values, db);
|
||||
|
||||
return {
|
||||
[Op.in]: mapVal(values, db),
|
||||
};
|
||||
if (items.length) {
|
||||
return Sequelize.literal(`"${ctx.model.name}"."tableoid" IN (${joinValues(items)})`);
|
||||
} else {
|
||||
return Sequelize.literal(`1 = 2`);
|
||||
}
|
||||
},
|
||||
|
||||
$childNotIn(values, ctx: any) {
|
||||
const db = ctx.db;
|
||||
const items = filterItems(values, db);
|
||||
|
||||
return {
|
||||
[Op.notIn]: mapVal(values, db),
|
||||
};
|
||||
if (items.length) {
|
||||
return Sequelize.literal(`"${ctx.model.name}"."tableoid" NOT IN (${joinValues(items)})`);
|
||||
} else {
|
||||
return Sequelize.literal(`1 = 1`);
|
||||
}
|
||||
},
|
||||
} as Record<string, any>;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { flatten } from 'flat';
|
||||
import lodash from 'lodash';
|
||||
import {
|
||||
Association,
|
||||
@ -18,6 +19,7 @@ import { Collection } from './collection';
|
||||
import { Database } from './database';
|
||||
import mustHaveFilter from './decorators/must-have-filter-decorator';
|
||||
import { transactionWrapperBuilder } from './decorators/transaction-decorator';
|
||||
import { EagerLoadingTree } from './eager-loading/eager-loading-tree';
|
||||
import { ArrayFieldRepository } from './field-repository/array-field-repository';
|
||||
import { ArrayField, RelationField } from './fields';
|
||||
import FilterParser from './filter-parser';
|
||||
@ -31,8 +33,6 @@ import { HasOneRepository } from './relation-repository/hasone-repository';
|
||||
import { RelationRepository } from './relation-repository/relation-repository';
|
||||
import { updateAssociations, updateModelByValues } from './update-associations';
|
||||
import { UpdateGuard } from './update-guard';
|
||||
import { EagerLoadingTree } from './eager-loading/eager-loading-tree';
|
||||
import { flatten } from 'flat';
|
||||
|
||||
const debug = require('debug')('noco-database');
|
||||
|
||||
|
@ -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'],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ const useCreateCollectionField = (record) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const AddFieldAction = ({ item: record }) => {
|
||||
export const AddFieldAction = ({ item: record, database }) => {
|
||||
return (
|
||||
<AddCollectionFieldAction
|
||||
trigger={['click']}
|
||||
@ -24,6 +24,7 @@ export const AddFieldAction = ({ item: record }) => {
|
||||
},
|
||||
}}
|
||||
item={record}
|
||||
database={database}
|
||||
scope={{
|
||||
useCancelAction,
|
||||
useCreateCollectionField: () => useCreateCollectionField(record),
|
||||
|
@ -5,6 +5,7 @@ import { uid } from '@formily/shared';
|
||||
import {
|
||||
Action,
|
||||
Checkbox,
|
||||
collection,
|
||||
CollectionCategroriesContext,
|
||||
CollectionField,
|
||||
CollectionProvider,
|
||||
@ -19,7 +20,6 @@ import {
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
Select,
|
||||
collection,
|
||||
useCollectionManager,
|
||||
useCompile,
|
||||
useCurrentAppInfo,
|
||||
@ -190,6 +190,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
|
||||
const [collapse, setCollapse] = useState(false);
|
||||
const { t } = useGCMTranslation();
|
||||
const compile = useCompile();
|
||||
const database = useCurrentAppInfo();
|
||||
const portsData = groupBy(ports.items, (v) => {
|
||||
if (
|
||||
v.isForeignKey ||
|
||||
@ -292,6 +293,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
|
||||
...property,
|
||||
title,
|
||||
},
|
||||
database,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
|
Loading…
Reference in New Issue
Block a user