feat: support to use variables to set default value (#1726)

* fix: fix the default value being overwriitten

* feat: supports association fields to set default value

* feat: support to toggle multiple

* feat: support to use variables to set default value

* refactor: migrate hooks

* fix: fix the title disappearing

* feat: suport to use current-user variable

* fix: fix error

* fix: one to one and one to many should not support to set default value

* fix: fix error when searching

* style: set the width of modal

* fix: should not support to set default value for system fields

* refactor: fix lint error

* style: optimize inputNumber width

* refactor: extract to one function

* refactor: extract function

* fix: should not display default when field is readPretty

* style: optimize width of select

* fix: avoid error

* fix: should not display multiple option when subtable

* fix: should not display default when attachment

* fix: chinaRegion

* chore: optimize code

* fix: build error

* fix: multiple should be true by default

* refactor: optimize code

* fix: fix Select multiple mode

* refactor: optimize code

* fix: width
This commit is contained in:
被雨水过滤的空气-Rairn 2023-04-23 18:27:21 +08:00 committed by GitHub
parent eac3c6f0c8
commit b1068f2d51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 878 additions and 458 deletions

View File

@ -1,6 +1,7 @@
import { createForm, onFormValuesChange } from '@formily/core'; import { createForm, onFormValuesChange } from '@formily/core';
import { useField } from '@formily/react'; import { useField } from '@formily/react';
import { autorun } from '@formily/reactive'; import { autorun } from '@formily/reactive';
import { forEach } from '@nocobase/utils/client';
import { Spin } from 'antd'; import { Spin } from 'antd';
import React, { createContext, useContext, useEffect, useMemo } from 'react'; import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { RecordProvider } from '../record-provider'; import { RecordProvider } from '../record-provider';
@ -35,7 +36,14 @@ const InternalFormFieldProvider = (props) => {
// 当使用数据模板时,会 formBlockCtx.form.values 会被整体赋值,这时候需要同步到 form.values // 当使用数据模板时,会 formBlockCtx.form.values 会被整体赋值,这时候需要同步到 form.values
useEffect(() => { useEffect(() => {
const dispose = autorun(() => { const dispose = autorun(() => {
form.values = formBlockCtx?.form?.values[fieldName] || form.values; const data = formBlockCtx?.form?.values[fieldName] || {};
// 先清空表单值,再赋值,避免当值为空时,表单未被清空
form.reset();
forEach(data, (value, key) => {
if (value) {
form.values[key] = value;
}
});
}); });
return dispose; return dispose;

View File

@ -7,12 +7,18 @@ import { useActionContext, useCompile, useComponent, useFormBlockContext, useRec
import { CollectionFieldProvider } from './CollectionFieldProvider'; import { CollectionFieldProvider } from './CollectionFieldProvider';
import { useCollectionField } from './hooks'; import { useCollectionField } from './hooks';
type Props = {
component: any;
children?: React.ReactNode;
};
// TODO: 初步适配 // TODO: 初步适配
const InternalField: React.FC = (props) => { const InternalField: React.FC = (props: Props) => {
const { component } = props;
const field = useField<Field>(); const field = useField<Field>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { uiSchema, defaultValue } = useCollectionField(); const { uiSchema, defaultValue } = useCollectionField();
const component = useComponent(uiSchema?.['x-component'] || 'Input'); const Component = useComponent(component || uiSchema?.['x-component'] || 'Input');
const compile = useCompile(); const compile = useCompile();
const setFieldProps = (key, value) => { const setFieldProps = (key, value) => {
field[key] = typeof field[key] === 'undefined' ? value : field[key]; field[key] = typeof field[key] === 'undefined' ? value : field[key];
@ -57,12 +63,12 @@ const InternalField: React.FC = (props) => {
field.dataSource = uiSchema.enum; field.dataSource = uiSchema.enum;
const originalProps = compile(uiSchema['x-component-props']) || {}; const originalProps = compile(uiSchema['x-component-props']) || {};
const componentProps = merge(originalProps, field.componentProps || {}); const componentProps = merge(originalProps, field.componentProps || {});
field.component = [component, componentProps]; field.component = [Component, componentProps];
}, [JSON.stringify(uiSchema)]); }, [JSON.stringify(uiSchema)]);
if (!uiSchema) { if (!uiSchema) {
return null; return null;
} }
return React.createElement(component, props, props.children); return React.createElement(Component, props, props.children);
}; };
export const InternalFallbackField = () => { export const InternalFallbackField = () => {

View File

@ -13,8 +13,8 @@ import { useResourceActionContext, useResourceContext } from '../ResourceActionP
import { useCancelAction } from '../action-hooks'; import { useCancelAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import * as components from './components'; import * as components from './components';
import { templateOptions } from './templates';
import { TemplateSummay } from './components/TemplateSummay'; import { TemplateSummay } from './components/TemplateSummay';
import { templateOptions } from './templates';
const getSchema = (schema, category, compile): ISchema => { const getSchema = (schema, category, compile): ISchema => {
if (!schema) { if (!schema) {

View File

@ -32,9 +32,14 @@ export const multipleSelect: IField = {
operators: operators.array, operators: operators.array,
}, },
schemaInitialize(schema: ISchema, { block }) { 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)) { if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {}; props['ellipsis'] = true;
schema['x-component-props']['ellipsis'] = true;
} }
}, },
}; };

View File

@ -1,4 +1,5 @@
import { registerValidateRules } from '@formily/core'; import { registerValidateRules } from '@formily/core';
import { ISchema } from '@formily/react';
import { i18n } from '../../i18n'; import { i18n } from '../../i18n';
import { defaultProps, operators, unique } from './properties'; import { defaultProps, operators, unique } from './properties';
import { IField } from './types'; import { IField } from './types';
@ -67,6 +68,13 @@ export const percent: IField = {
}, },
}, },
}, },
schemaInitialize(schema: ISchema, { field, block, readPretty, action }) {
const props = (schema['x-component-props'] = schema['x-component-props'] || {});
schema['x-component-props'].style = {
...(props.style || {}),
width: '100%',
};
},
availableTypes: ['float'], availableTypes: ['float'],
hasDefaultValue: true, hasDefaultValue: true,
properties: { properties: {

View File

@ -29,9 +29,14 @@ export const select: IField = {
operators: operators.enumType, operators: operators.enumType,
}, },
schemaInitialize(schema: ISchema, { block }) { 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)) { if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {}; props['ellipsis'] = true;
schema['x-component-props']['ellipsis'] = true;
} }
}, },
}; };

View File

@ -14,7 +14,7 @@ import {
useSortFields, useSortFields,
} from '../../../collection-manager'; } from '../../../collection-manager';
import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields'; import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
import { useCompile, useDesignable, useFieldComponentOptions, useFieldTitle } from '../../hooks'; import { useCompile, useDesignable, useFieldComponentOptions, useFieldTitle } from '../../hooks';
import { removeNullCondition } from '../filter'; import { removeNullCondition } from '../filter';
import { RemoteSelect, RemoteSelectProps } from '../remote-select'; import { RemoteSelect, RemoteSelectProps } from '../remote-select';
@ -94,7 +94,7 @@ export const AssociationSelect = InternalAssociationSelect as unknown as Associa
AssociationSelect.Designer = function Designer() { AssociationSelect.Designer = function Designer() {
const { getCollectionFields, getInterface, getCollectionJoinField, getCollection } = useCollectionManager(); const { getCollectionFields, getInterface, getCollectionJoinField, getCollection } = useCollectionManager();
const { getField, template } = useCollection(); const { getField } = useCollection();
const { form } = useFormBlockContext(); const { form } = useFormBlockContext();
const field = useField<Field>(); const field = useField<Field>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
@ -390,41 +390,52 @@ AssociationSelect.Designer = function Designer() {
}} }}
/> />
)} )}
{form && !form?.readPretty && collectionField?.uiSchema?.type && ( {form &&
<SchemaSettings.ModalItem !form?.readPretty &&
title={t('Set default value')} isShowDefaultValue(collectionField, getInterface) &&
components={{ ArrayCollapse, FormLayout }} !isPatternDisabled(fieldSchema) && (
schema={ <SchemaSettings.ModalItem
{ title={t('Set default value')}
type: 'object', components={{ ArrayCollapse, FormLayout }}
title: t('Set default value'), width={800}
properties: { schema={
default: { {
...collectionField.uiSchema, type: 'object',
name: 'default', title: t('Set default value'),
title: t('Default value'), properties: {
'x-decorator': 'FormItem', default: {
default: fieldSchema.default || collectionField.defaultValue, ...(fieldSchema || {}),
'x-decorator': 'FormItem',
'x-component-props': {
...fieldSchema['x-component-props'],
component: collectionField?.target ? 'AssociationSelect' : undefined,
service: {
resource: collectionField?.target,
},
},
name: 'default',
title: t('Default value'),
default: fieldSchema.default || collectionField.defaultValue,
},
}, },
}, } as ISchema
} as ISchema
}
onSubmit={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
if (field.value !== v.default) {
field.value = v.default;
} }
fieldSchema.default = v.default; onSubmit={(v) => {
schema.default = v.default; const schema: ISchema = {
dn.emit('patch', { ['x-uid']: fieldSchema['x-uid'],
schema, };
}); if (field.value !== v.default) {
refresh(); field.value = v.default;
}} }
/> fieldSchema.default = v.default;
)} schema.default = v.default;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
{form && !isSubFormAssociationField && fieldComponentOptions && ( {form && !isSubFormAssociationField && fieldComponentOptions && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
title={t('Field component')} title={t('Field component')}
@ -474,6 +485,36 @@ AssociationSelect.Designer = function Designer() {
}} }}
/> />
)} )}
{form &&
!form?.readPretty &&
['o2m', 'm2m'].includes(collectionField.interface) &&
fieldSchema['x-component'] !== 'TableField' && (
<SchemaSettings.SwitchItem
key="multiple"
title={t('Multiple')}
checked={
fieldSchema['x-component-props']?.multiple === undefined
? true
: fieldSchema['x-component-props'].multiple
}
onChange={(value) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
field.componentProps = field.componentProps || {};
fieldSchema['x-component-props'].multiple = value;
field.componentProps.multiple = value;
schema['x-component-props'] = fieldSchema['x-component-props'];
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Set the data scope')} title={t('Set the data scope')}
schema={ schema={
@ -594,7 +635,7 @@ AssociationSelect.Designer = function Designer() {
}); });
}} }}
/> />
{form && !form?.readPretty && fieldSchema?.['x-component-props']?.['pattern-disable'] != true && ( {form && !form?.readPretty && !isPatternDisabled(fieldSchema) && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
key="pattern" key="pattern"
title={t('Pattern')} title={t('Pattern')}

View File

@ -7,13 +7,66 @@ 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 useFilterOptions = (collectionName: string) => { export const useGetFilterOptions = () => {
const { getCollectionFields } = useCollectionManager(); const { getCollectionFields } = useCollectionManager();
const fields = getCollectionFields(collectionName);
const options=useFilterFieldOptions(fields)
const compile = useCompile(); const compile = useCompile();
const { getChildrenCollections } = useCollectionManager(); const { getChildrenCollections } = useCollectionManager();
const collection = useCollection(); 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); const childrenCollections = getChildrenCollections(collection.name);
if (childrenCollections.length > 0 && !options.find((v) => v.name == 'tableoid')) { if (childrenCollections.length > 0 && !options.find((v) => v.name == 'tableoid')) {
options.push({ options.push({
@ -54,6 +107,63 @@ export const useFilterOptions = (collectionName: string) => {
return options; return options;
}; };
export const useGetFilterFieldOptions = () => {
const fieldSchema = useFieldSchema();
const nonfilterable = fieldSchema?.['x-component-props']?.nonfilterable || [];
const { getCollectionFields, getInterface } = useCollectionManager();
const field2option = (field, depth) => {
if (nonfilterable.length && depth === 1 && nonfilterable.includes(field.name)) {
return;
}
if (!field.interface) {
return;
}
const fieldInterface = getInterface(field.interface);
if (!fieldInterface.filterable) {
return;
}
const { nested, children, operators } = fieldInterface.filterable;
const option = {
name: field.name,
type: field.type,
target: field.target,
title: field?.uiSchema?.title || field.name,
schema: field?.uiSchema,
operators:
operators?.filter?.((operator) => {
return !operator?.visible || operator.visible(field);
}) || [],
};
if (field.target && depth > 2) {
return;
}
if (depth > 2) {
return option;
}
if (children?.length) {
option['children'] = children;
}
if (nested) {
const targetFields = getCollectionFields(field.target);
const options = getOptions(targetFields, depth + 1).filter(Boolean);
option['children'] = option['children'] || [];
option['children'].push(...options);
}
return option;
};
const getOptions = (fields, depth) => {
const options = [];
fields.forEach((field) => {
const option = field2option(field, depth);
if (option) {
options.push(option);
}
});
return options;
};
return (fields) => getOptions(fields, 1);
};
export const useFilterFieldOptions = (fields) => { export const useFilterFieldOptions = (fields) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const nonfilterable = fieldSchema?.['x-component-props']?.nonfilterable || []; const nonfilterable = fieldSchema?.['x-component-props']?.nonfilterable || [];

View File

@ -22,7 +22,8 @@ const findOption = (dataIndex = [], options) => {
export const useValues = () => { export const useValues = () => {
const field = useField<any>(); const field = useField<any>();
const { options } = useContext(FilterContext); const { options } = useContext(FilterContext) || {};
const data2value = () => { const data2value = () => {
field.value = flat.unflatten({ field.value = flat.unflatten({
[`${field.data.dataIndex?.join('.')}.${field.data?.operator?.value}`]: field.data?.value, [`${field.data.dataIndex?.join('.')}.${field.data?.operator?.value}`]: field.data?.value,
@ -32,7 +33,7 @@ export const useValues = () => {
field.data = field.data || {}; field.data = field.data || {};
const values = flat(field.value); const values = flat(field.value);
const path = Object.keys(values).shift() || ''; const path = Object.keys(values).shift() || '';
if (!path) { if (!path || !options) {
return; return;
} }
const [fieldPath = '', otherPath = ''] = path.split('.$'); const [fieldPath = '', otherPath = ''] = path.split('.$');
@ -50,7 +51,7 @@ export const useValues = () => {
useEffect(value2data, [field.path.entire]); useEffect(value2data, [field.path.entire]);
return { return {
fields: options, fields: options,
...field.data, ...(field?.data || {}),
setDataIndex(dataIndex) { setDataIndex(dataIndex) {
const option = findOption(dataIndex, options); const option = findOption(dataIndex, options);
const operator = option?.operators?.[0]; const operator = option?.operators?.[0];

View File

@ -1,16 +1,19 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { ArrayCollapse, FormLayout, FormItem as Item } from '@formily/antd'; import { ArrayCollapse, FormLayout, FormItem as Item } from '@formily/antd';
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { ISchema, observer, useField, useFieldSchema } from '@formily/react'; import { ISchema, Schema, observer, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import _ from 'lodash'; import _ from 'lodash';
import React, { useContext, useEffect } from 'react'; import React, { useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider'; import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider';
import { BlockRequestContext, useFilterByTk, useFormBlockContext } from '../../../block-provider'; import { BlockRequestContext, useFilterByTk, useFormBlockContext } from '../../../block-provider';
import { Collection, useCollection, useCollectionManager } from '../../../collection-manager'; import { Collection, CollectionFieldOptions, useCollection, useCollectionManager } from '../../../collection-manager';
import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields'; import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
import { VariableInput } from '../../../schema-settings/VariableInput/VariableInput';
import { isVariable, parseVariables, useVariablesCtx } from '../../common/utils/uitls';
import { SchemaComponent } from '../../core';
import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks'; import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks';
import { BlockItem } from '../block-item'; import { BlockItem } from '../block-item';
import { HTMLEncode } from '../input/shared'; import { HTMLEncode } from '../input/shared';
@ -33,15 +36,20 @@ const divWrap = (schema: ISchema) => {
export const FormItem: any = observer((props: any) => { export const FormItem: any = observer((props: any) => {
useEnsureOperatorsValid(); useEnsureOperatorsValid();
const field = useField(); const field = useField<Field>();
const ctx = useContext(BlockRequestContext); const ctx = useContext(BlockRequestContext);
const schema = useFieldSchema(); const schema = useFieldSchema();
const variablesCtx = useVariablesCtx();
useEffect(() => { useEffect(() => {
if (ctx?.block === 'form') { if (ctx?.block === 'form') {
ctx.field.data = ctx.field.data || {}; ctx.field.data = ctx.field.data || {};
ctx.field.data.activeFields = ctx.field.data.activeFields || new Set(); ctx.field.data.activeFields = ctx.field.data.activeFields || new Set();
ctx.field.data.activeFields.add(schema.name); ctx.field.data.activeFields.add(schema.name);
// 如果默认值是一个变量,则需要解析之后再显示出来
if (isVariable(schema?.default)) {
field.setInitialValue?.(parseVariables(schema.default, variablesCtx));
}
} }
}, []); }, []);
return ( return (
@ -81,6 +89,8 @@ FormItem.Designer = function Designer() {
const { t } = useTranslation(); const { t } = useTranslation();
const { dn, refresh, insertAdjacent } = useDesignable(); const { dn, refresh, insertAdjacent } = useDesignable();
const compile = useCompile(); const compile = useCompile();
const variablesCtx = useVariablesCtx();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const targetCollection = getCollection(collectionField?.target); const targetCollection = getCollection(collectionField?.target);
const interfaceConfig = getInterface(collectionField?.interface); const interfaceConfig = getInterface(collectionField?.interface);
@ -396,41 +406,91 @@ FormItem.Designer = function Designer() {
}} }}
/> />
)} )}
{form && !form?.readPretty && collectionField?.uiSchema?.type && ( {form &&
<SchemaSettings.ModalItem !form?.readPretty &&
title={t('Set default value')} isShowDefaultValue(collectionField, getInterface) &&
components={{ ArrayCollapse, FormLayout }} !isPatternDisabled(fieldSchema) && (
schema={ <SchemaSettings.ModalItem
{ title={t('Set default value')}
type: 'object', components={{ ArrayCollapse, FormLayout, VariableInput }}
title: t('Set default value'), width={800}
properties: { schema={
default: { {
...collectionField?.uiSchema, type: 'object',
name: 'default', title: t('Set default value'),
title: t('Default value'), properties: {
'x-decorator': 'FormItem', // 关系字段不支持设置变量
default: fieldSchema.default || collectionField?.defaultValue, default: collectionField?.target
? {
...(fieldSchema || {}),
'x-decorator': 'FormItem',
'x-component-props': {
...fieldSchema['x-component-props'],
component:
collectionField?.target && collectionField.interface !== 'chinaRegion'
? 'AssociationSelect'
: undefined,
service: {
resource: collectionField?.target,
},
},
name: 'default',
title: t('Default value'),
default: getFieldDefaultValue(fieldSchema, collectionField),
}
: {
...(fieldSchema || {}),
'x-decorator': 'FormItem',
'x-component': 'VariableInput',
'x-component-props': {
...(fieldSchema?.['x-component-props'] || {}),
collectionName: collectionField?.collectionName,
schema: collectionField?.uiSchema,
renderSchemaComponent: function Com(props) {
const s = _.cloneDeep(fieldSchema) || ({} as Schema);
s.title = '';
return (
<SchemaComponent
schema={{
...(s || {}),
'x-component-props': {
...s['x-component-props'],
onChange: props.onChange,
value: props.value,
defaultValue: getFieldDefaultValue(s, collectionField),
style: {
width: '100%',
},
},
}}
/>
);
},
},
name: 'default',
title: t('Default value'),
default: getFieldDefaultValue(fieldSchema, collectionField),
},
}, },
}, } as ISchema
} as ISchema
}
onSubmit={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
if (field.value !== v.default) {
field.value = v.default;
} }
fieldSchema.default = v.default; onSubmit={(v) => {
schema.default = v.default; const schema: ISchema = {
dn.emit('patch', { ['x-uid']: fieldSchema['x-uid'],
schema, };
}); if (field.value !== v.default) {
refresh(); field.value = parseVariables(v.default, variablesCtx);
}} }
/> fieldSchema.default = v.default;
)} schema.default = v.default;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
{form && !isSubFormAssociationField && fieldComponentOptions && ( {form && !isSubFormAssociationField && fieldComponentOptions && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
title={t('Field component')} title={t('Field component')}
@ -476,6 +536,36 @@ FormItem.Designer = function Designer() {
}} }}
/> />
)} )}
{form &&
!form?.readPretty &&
['o2m', 'm2m'].includes(collectionField.interface) &&
fieldSchema['x-component'] !== 'TableField' && (
<SchemaSettings.SwitchItem
key="multiple"
title={t('Multiple')}
checked={
fieldSchema['x-component-props']?.multiple === undefined
? true
: fieldSchema['x-component-props'].multiple
}
onChange={(value) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
field.componentProps = field.componentProps || {};
fieldSchema['x-component-props'].multiple = value;
field.componentProps.multiple = value;
schema['x-component-props'] = fieldSchema['x-component-props'];
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
{field.readPretty && options.length > 0 && fieldSchema['x-component'] === 'CollectionField' && ( {field.readPretty && options.length > 0 && fieldSchema['x-component'] === 'CollectionField' && (
<SchemaSettings.SwitchItem <SchemaSettings.SwitchItem
title={t('Enable link')} title={t('Enable link')}
@ -497,61 +587,58 @@ FormItem.Designer = function Designer() {
}} }}
/> />
)} )}
{form && {form && !form?.readPretty && collectionField?.interface !== 'o2m' && !isPatternDisabled(fieldSchema) && (
!form?.readPretty && <SchemaSettings.SelectItem
collectionField?.interface !== 'o2m' && key="pattern"
fieldSchema?.['x-component-props']?.['pattern-disable'] != true && ( title={t('Pattern')}
<SchemaSettings.SelectItem options={[
key="pattern" { label: t('Editable'), value: 'editable' },
title={t('Pattern')} { label: t('Readonly'), value: 'readonly' },
options={[ { label: t('Easy-reading'), value: 'read-pretty' },
{ label: t('Editable'), value: 'editable' }, ]}
{ label: t('Readonly'), value: 'readonly' }, value={readOnlyMode}
{ label: t('Easy-reading'), value: 'read-pretty' }, onChange={(v) => {
]} const schema: ISchema = {
value={readOnlyMode} ['x-uid']: fieldSchema['x-uid'],
onChange={(v) => { };
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
switch (v) { switch (v) {
case 'readonly': { case 'readonly': {
fieldSchema['x-read-pretty'] = false; fieldSchema['x-read-pretty'] = false;
fieldSchema['x-disabled'] = true; fieldSchema['x-disabled'] = true;
schema['x-read-pretty'] = false; schema['x-read-pretty'] = false;
schema['x-disabled'] = true; schema['x-disabled'] = true;
field.readPretty = false; field.readPretty = false;
field.disabled = true; field.disabled = true;
break; break;
}
case 'read-pretty': {
fieldSchema['x-read-pretty'] = true;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = true;
schema['x-disabled'] = false;
field.readPretty = true;
break;
}
default: {
fieldSchema['x-read-pretty'] = false;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = false;
schema['x-disabled'] = false;
field.readPretty = false;
field.disabled = false;
break;
}
} }
case 'read-pretty': {
fieldSchema['x-read-pretty'] = true;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = true;
schema['x-disabled'] = false;
field.readPretty = true;
break;
}
default: {
fieldSchema['x-read-pretty'] = false;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = false;
schema['x-disabled'] = false;
field.readPretty = false;
field.disabled = false;
break;
}
}
dn.emit('patch', { dn.emit('patch', {
schema, schema,
}); });
dn.refresh(); dn.refresh();
}} }}
/> />
)} )}
{options.length > 0 && fieldSchema['x-component'] === 'CollectionField' && ( {options.length > 0 && fieldSchema['x-component'] === 'CollectionField' && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
key="title-field" key="title-field"
@ -597,3 +684,7 @@ function isFileCollection(collection: Collection) {
} }
FormItem.FilterFormDesigner = FilterFormDesigner; FormItem.FilterFormDesigner = FilterFormDesigner;
export function getFieldDefaultValue(fieldSchema: ISchema, collectionField: CollectionFieldOptions) {
return fieldSchema?.default || collectionField?.defaultValue;
}

View File

@ -7,7 +7,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useFilterByTk, useFormBlockContext } from '../../../block-provider'; import { useFilterByTk, useFormBlockContext } from '../../../block-provider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
import { SchemaSettings } from '../../../schema-settings'; import { SchemaSettings, isPatternDisabled } from '../../../schema-settings';
import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks'; import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks';
import { useOperatorList } from '../filter/useOperators'; import { useOperatorList } from '../filter/useOperators';
@ -448,10 +448,7 @@ export const EditPattern = () => {
readOnlyMode = 'read-pretty'; readOnlyMode = 'read-pretty';
} }
return form && return form && !form?.readPretty && collectionField?.interface !== 'o2m' && !isPatternDisabled(fieldSchema) ? (
!form?.readPretty &&
collectionField?.interface !== 'o2m' &&
fieldSchema?.['x-component-props']?.['pattern-disable'] != true ? (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
key="pattern" key="pattern"
title={t('Pattern')} title={t('Pattern')}

View File

@ -1,10 +1,12 @@
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { forEach } from '@nocobase/utils/client';
import { Select } from 'antd'; import { Select } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../../api-client'; import { useAPIClient } from '../../../api-client';
import { findFormBlock } from '../../../block-provider'; import { findFormBlock } from '../../../block-provider';
import { useCollectionManager } from '../../../collection-manager';
interface ITemplate { interface ITemplate {
items: { items: {
@ -40,26 +42,41 @@ const useDataTemplates = () => {
export const Templates = ({ style = {}, form }) => { export const Templates = ({ style = {}, form }) => {
const { templates, display, enabled, defaultTemplate } = useDataTemplates(); const { templates, display, enabled, defaultTemplate } = useDataTemplates();
const { getCollectionField } = useCollectionManager();
const [value, setValue] = React.useState(defaultTemplate?.key || 'none'); const [value, setValue] = React.useState(defaultTemplate?.key || 'none');
const api = useAPIClient(); const api = useAPIClient();
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
if (defaultTemplate) { if (defaultTemplate) {
fetchTemplateData(api, defaultTemplate).then((data) => { fetchTemplateData(api, defaultTemplate)
if (form) { .then((data) => {
form.values = data; if (form) {
} forEach(data, (value, key) => {
}); if (value) {
form.values[key] = value;
}
});
}
})
.catch((err) => {
console.error(err);
});
} }
}, []); }, []);
const handleChange = useCallback(async (value, option) => { const handleChange = useCallback(async (value, option) => {
setValue(value); setValue(value);
if (option.key !== 'none') { if (option.key !== 'none') {
if (form) { fetchTemplateData(api, option).then((data) => {
form.values = await fetchTemplateData(api, option); if (form) {
} forEach(data, (value, key) => {
if (value) {
form.values[key] = value;
}
});
}
});
} else { } else {
form?.reset(); form?.reset();
} }

View File

@ -5,7 +5,7 @@ import { isValid } from '@formily/shared';
import { Button, Input, Popover } from 'antd'; import { Button, Input, Popover } from 'antd';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { hasIcon, Icon, icons } from '../../../icon'; import { Icon, hasIcon, icons } from '../../../icon';
function IconField(props: any) { function IconField(props: any) {
const layout = useFormLayout(); const layout = useFormLayout();

View File

@ -236,7 +236,6 @@ const Drawer: React.FunctionComponent<{
fieldSchema, fieldSchema,
options, options,
}) => { }) => {
console.log('collectionField', options);
const getFilter = () => { const getFilter = () => {
const targetKey = collectionField.targetKey || 'id'; const targetKey = collectionField.targetKey || 'id';
const list = options.map((option) => option[targetKey]).filter(Boolean); const list = options.map((option) => option[targetKey]).filter(Boolean);

View File

@ -38,7 +38,7 @@ const InternalRemoteSelect = connect(
const firstRun = useRef(false); const firstRun = useRef(false);
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { getField } = useCollection(); const { getField } = useCollection();
const { getCollectionJoinField } = useCollectionManager(); const { getCollectionJoinField, getInterface } = useCollectionManager();
const collectionField = getField(fieldSchema.name); const collectionField = getField(fieldSchema.name);
const targetField = const targetField =
_targetField || _targetField ||
@ -46,6 +46,13 @@ const InternalRemoteSelect = connect(
fieldNames?.label && fieldNames?.label &&
getCollectionJoinField(`${collectionField.target}.${fieldNames.label}`)); getCollectionJoinField(`${collectionField.target}.${fieldNames.label}`));
const operator = useMemo(() => {
if (targetField?.interface) {
return getInterface(targetField.interface)?.filterable?.operators[0].value || '$includes';
}
return '$includes';
}, [targetField]);
const mapOptionsToTags = useCallback( const mapOptionsToTags = useCallback(
(options) => { (options) => {
try { try {
@ -138,7 +145,7 @@ const InternalRemoteSelect = connect(
filter: mergeFilter([ filter: mergeFilter([
{ {
[fieldNames.label]: { [fieldNames.label]: {
$includes: search, [operator]: search,
}, },
}, },
service?.params?.filter, service?.params?.filter,

View File

@ -71,24 +71,18 @@ const InternalSelect = connect(
(props: Props) => { (props: Props) => {
const { objectValue, value, ...others } = props; const { objectValue, value, ...others } = props;
let mode: any = props.multiple ? 'multiple' : props.mode; let mode: any = props.multiple ? 'multiple' : props.mode;
if (mode === 'links') { if (mode && !['multiple', 'tags'].includes(mode)) {
mode = undefined; mode = undefined;
} }
const toValue = (v) => {
if (['multiple', 'tags'].includes(mode)) {
return v || [];
}
return v;
};
if (objectValue) { if (objectValue) {
return <ObjectSelect {...others} value={toValue(value)} mode={mode} />; return <ObjectSelect {...others} value={value} mode={mode} />;
} }
return ( return (
<AntdSelect <AntdSelect
showSearch showSearch
filterOption={filterOption} filterOption={filterOption}
allowClear allowClear
value={toValue(value)} value={value}
{...others} {...others}
onChange={(changed) => { onChange={(changed) => {
props.onChange?.(changed === undefined ? null : changed); props.onChange?.(changed === undefined ? null : changed);

View File

@ -1,172 +1,13 @@
import React from 'react'; import React from 'react';
import { useCompile } from '../..'; import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks/useVariableOptions';
import { useValues } from '../filter/useValues';
import { Variable } from '../variable'; import { Variable } from '../variable';
import { useUserVariable } from './hooks/useUserVariable';
const useVariableOptions = () => {
const { operator, schema } = useValues();
const operatorValue = operator?.value || '';
const userVariable = useUserVariable({ schema, operator });
if (!operator || !schema) return [];
const disabled = !['DatePicker', 'DatePicker.RangePicker'].includes(schema['x-component']);
const dateOptions = [
{
key: 'now',
value: 'now',
label: `{{t("Now")}}`,
disabled: schema['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween',
},
{
key: 'yesterday',
value: 'yesterday',
label: `{{t("Yesterday")}}`,
disabled,
},
{
key: 'today',
value: 'today',
label: `{{t("Today")}}`,
disabled,
},
{
key: 'tomorrow',
value: 'tomorrow',
label: `{{t("Tomorrow")}}`,
disabled,
},
{
key: 'lastIsoWeek',
value: 'lastIsoWeek',
label: `{{t("Last week")}}`,
disabled,
},
{
key: 'thisIsoWeek',
value: 'thisIsoWeek',
label: `{{t("This week")}}`,
disabled,
},
{
key: 'nextIsoWeek',
value: 'nextIsoWeek',
label: `{{t("Next week")}}`,
disabled,
},
{
key: 'lastMonth',
value: 'lastMonth',
label: `{{t("Last month")}}`,
disabled,
},
{
key: 'thisMonth',
value: 'thisMonth',
label: `{{t("This month")}}`,
disabled,
},
{
key: 'nextMonth',
value: 'nextMonth',
label: `{{t("Next month")}}`,
disabled,
},
{
key: 'lastQuarter',
value: 'lastQuarter',
label: `{{t("Last quarter")}}`,
disabled,
},
{
key: 'thisQuarter',
value: 'thisQuarter',
label: `{{t("This quarter")}}`,
disabled,
},
{
key: 'nextQuarter',
value: 'nextQuarter',
label: `{{t("Next quarter")}}`,
disabled,
},
{
key: 'lastYear',
value: 'lastYear',
label: `{{t("Last year")}}`,
disabled,
},
{
key: 'thisYear',
value: 'thisYear',
label: `{{t("This year")}}`,
disabled,
},
{
key: 'nextYear',
value: 'nextYear',
label: `{{t("Next year")}}`,
disabled,
},
{
key: 'last7Days',
value: 'last7Days',
label: `{{t("Last 7 days")}}`,
disabled,
},
{
key: 'next7Days',
value: 'next7Days',
label: `{{t("Next 7 days")}}`,
disabled,
},
{
key: 'last30Days',
value: 'last30Days',
label: `{{t("Last 30 days")}}`,
disabled,
},
{
key: 'next30Days',
value: 'next30Days',
label: `{{t("Next 30 days")}}`,
disabled,
},
{
key: 'last90Days',
value: 'last90Days',
label: `{{t("Last 90 days")}}`,
disabled,
},
{
key: 'next90Days',
value: 'next90Days',
label: `{{t("Next 90 days")}}`,
disabled,
},
];
return [
userVariable,
{
label: `{{t("Date variables")}}`,
value: '$date',
key: '$date',
disabled: dateOptions.every((option) => option.disabled),
children: dateOptions,
},
];
};
export function FilterDynamicComponent(props) { export function FilterDynamicComponent(props) {
const { value, onChange, renderSchemaComponent } = props; const { value, onChange, renderSchemaComponent } = props;
const options = useVariableOptions(); const options = useVariableOptions();
const compile = useCompile();
const scope = compile(options);
return ( return (
<Variable.Input value={value} onChange={onChange} scope={scope}> <Variable.Input value={value} onChange={onChange} scope={options}>
{renderSchemaComponent()} {renderSchemaComponent()}
</Variable.Input> </Variable.Input>
); );

View File

@ -4,10 +4,10 @@ import { css } from '@emotion/css';
import { ArrayField, Field } from '@formily/core'; import { ArrayField, Field } from '@formily/core';
import { RecursionField, Schema, observer, useField, useFieldSchema } from '@formily/react'; import { RecursionField, Schema, observer, useField, useFieldSchema } from '@formily/react';
import { reaction } from '@formily/reactive'; import { reaction } from '@formily/reactive';
import { useEventListener, useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Table as AntdTable, TableColumnProps } from 'antd'; import { Table as AntdTable, TableColumnProps } from 'antd';
import { default as classNames, default as cls } from 'classnames'; import { default as classNames, default as cls } from 'classnames';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DndContext, useDesignable, useTableSize } from '../..'; import { DndContext, useDesignable, useTableSize } from '../..';
import { import {
@ -213,7 +213,7 @@ export const Table: any = observer((props: any) => {
useEffect(() => { useEffect(() => {
if (treeTable !== false) { if (treeTable !== false) {
const keys = getIdsWithChildren(field.value?.slice()); const keys = getIdsWithChildren(field.value?.slice?.());
setAllIncludesChildren(keys); setAllIncludesChildren(keys);
} }
}, [field.value]); }, [field.value]);

View File

@ -1,56 +0,0 @@
import { useFilterOptions } from '../../filter';
interface GetOptionsParams {
schema: any;
operator: string;
maxDepth: number;
count?: number;
}
const useOptions = (collectionName: string, { schema, operator, maxDepth, count = 1 }: GetOptionsParams) => {
if (count > maxDepth) {
return [];
}
const result = useFilterOptions(collectionName).map((option) => {
if ((option.type !== 'belongsTo' && option.type !== 'hasOne') || !option.target) {
return {
key: option.name,
value: option.name,
label: option.title,
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
disabled: schema?.['x-component'] !== option.schema?.['x-component'],
};
}
const children =
useOptions(option.target, {
schema,
operator,
maxDepth,
count: count + 1,
}) || [];
return {
key: option.name,
value: option.name,
label: option.title,
children,
disabled: children.every((child) => child.disabled),
};
});
return result;
};
export const useUserVariable = ({ schema, operator }) => {
const options = useOptions('users', { schema, operator, maxDepth: 3 }) || [];
return {
label: `{{t("Current user")}}`,
value: '$user',
key: '$user',
disabled: options.every((option) => option.disabled),
children: options,
};
};

View File

@ -1,7 +1,7 @@
import { CloseCircleFilled } from '@ant-design/icons'; import { CloseCircleFilled } from '@ant-design/icons';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useForm } from '@formily/react'; import { useForm } from '@formily/react';
import { Button, Cascader, DatePicker, Input as AntInput, InputNumber, Select, Tag } from 'antd'; import { Input as AntInput, Cascader, DatePicker, InputNumber, Select, Tag } from 'antd';
import moment from 'moment'; import moment from 'moment';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -117,7 +117,7 @@ export function Input(props) {
const compile = useCompile(); const compile = useCompile();
const form = useForm(); const form = useForm();
const { value = '', scope, onChange, children, button, useTypedConstant } = props; const { value = '', scope, onChange, children, button, useTypedConstant, style } = props;
const parsed = parseValue(value); const parsed = parseValue(value);
const isConstant = typeof parsed === 'string'; const isConstant = typeof parsed === 'string';
const type = isConstant ? parsed : ''; const type = isConstant ? parsed : '';
@ -171,6 +171,7 @@ export function Input(props) {
return ( return (
<AntInput.Group <AntInput.Group
compact compact
style={style}
className={css` className={css`
width: auto; width: auto;
display: flex !important; display: flex !important;
@ -186,67 +187,71 @@ export function Input(props) {
} }
`} `}
> >
{variable ? ( <div style={{ flex: 1 }}>
<div {variable ? (
className={css`
position: relative;
line-height: 0;
&:hover {
.ant-select-clear {
opacity: 0.8;
}
}
.ant-input {
overflow: auto;
white-space: nowrap;
${disabled ? '' : 'padding-right: 28px;'}
.ant-tag {
display: inline;
line-height: 19px;
margin: 0;
padding: 2px 7px;
border-radius: 10px;
}
}
`}
>
<div <div
onInput={(e) => e.preventDefault()} className={css`
onKeyDown={(e) => { position: relative;
if (e.key !== 'Backspace') { line-height: 0;
e.preventDefault();
return; &:hover {
.ant-select-clear {
opacity: 0.8;
}
} }
onChange(null);
}} .ant-input {
className={cx('ant-input', { 'ant-input-disabled': disabled })} overflow: auto;
contentEditable={!disabled} white-space: nowrap;
suppressContentEditableWarning ${disabled ? '' : 'padding-right: 28px;'}
.ant-tag {
display: inline;
line-height: 19px;
margin: 0;
padding: 2px 7px;
border-radius: 10px;
}
}
`}
> >
<Tag contentEditable={false} color="blue"> <div
{variableText} onInput={(e) => e.preventDefault()}
</Tag> onKeyDown={(e) => {
</div> if (e.key !== 'Backspace') {
{!disabled ? ( e.preventDefault();
<span return;
className={cx( }
'ant-select-clear', onChange(null);
css` }}
user-select: 'none'; className={cx('ant-input', { 'ant-input-disabled': disabled })}
`, contentEditable={!disabled}
)} suppressContentEditableWarning
unselectable="on"
aria-hidden
onClick={() => onChange(null)}
> >
<CloseCircleFilled /> <Tag contentEditable={false} color="blue">
</span> {variableText}
) : null} </Tag>
</div> </div>
) : children ?? <ConstantComponent value={value} onChange={onChange} />} {!disabled ? (
<span
className={cx(
'ant-select-clear',
css`
user-select: 'none';
`,
)}
unselectable="on"
aria-hidden
onClick={() => onChange(null)}
>
<CloseCircleFilled />
</span>
) : null}
</div>
) : (
children ?? <ConstantComponent value={value} onChange={onChange} />
)}
</div>
{options.length > 1 ? ( {options.length > 1 ? (
<Cascader <Cascader
options={options} options={options}
@ -254,9 +259,7 @@ export function Input(props) {
onChange={onSwitch} onChange={onSwitch}
changeOnSelect changeOnSelect
> >
{button ?? ( {button ?? <XButton type={variable ? 'primary' : 'default'} />}
<XButton type={variable ? 'primary' : 'default'} />
)}
</Cascader> </Cascader>
) : null} ) : null}
</AntInput.Group> </AntInput.Group>

View File

@ -1,7 +1,46 @@
import { every, some, findIndex } from 'lodash';
import flat from 'flat'; import flat from 'flat';
import _, { every, findIndex, some } from 'lodash';
import moment from 'moment';
import { useCurrentUserContext } from '../../../user';
import jsonLogic from '../../common/utils/logic'; import jsonLogic from '../../common/utils/logic';
type VariablesCtx = {
/** 当前登录的用户 */
$user: Record<string, any>;
$date: Record<string, any>;
};
export const useVariablesCtx = (): VariablesCtx => {
const { data } = useCurrentUserContext() || {};
return {
$user: data?.data || {},
$date: {
now: () => moment().toISOString(),
},
};
};
export const isVariable = (str: unknown) => {
if (typeof str !== 'string') {
return false;
}
const regex = /{{(.*?)}}/;
const matches = str?.match?.(regex);
return matches ? true : false;
};
export const parseVariables = (str: string, ctx: VariablesCtx) => {
const regex = /{{(.*?)}}/;
const matches = str?.match?.(regex);
if (matches) {
const result = _.get(ctx, matches[1]);
return _.isFunction(result) ? result() : result;
} else {
return str;
}
};
function getInnermostKeyAndValue(obj) { function getInnermostKeyAndValue(obj) {
if (typeof obj !== 'object' || obj === null) { if (typeof obj !== 'object' || obj === null) {
return null; return null;
@ -17,7 +56,8 @@ function getInnermostKeyAndValue(obj) {
} }
return null; return null;
} }
const getValue = (str, values) => {
const getValue = (str: string, values) => {
const regex = /{{(.*?)}}/; const regex = /{{(.*?)}}/;
const matches = str?.match?.(regex); const matches = str?.match?.(regex);
if (matches) { if (matches) {

View File

@ -3,12 +3,12 @@ import { get } from 'lodash';
import { useContext } from 'react'; import { useContext } from 'react';
export const useComponent = (component: any, defaults?: any) => { export const useComponent = (component: any, defaults?: any) => {
const { components } = useContext(SchemaOptionsContext);
if (!component) { if (!component) {
return defaults; return defaults;
} }
if (typeof component !== 'string') { if (typeof component !== 'string') {
return component; return component;
} }
const { components } = useContext(SchemaOptionsContext);
return get(components, component) || defaults; return get(components, component) || defaults;
}; };

View File

@ -2,7 +2,14 @@ import React from 'react';
import { Variable } from '../../schema-component'; import { Variable } from '../../schema-component';
import { useVariableOptions } from './Variables'; import { useVariableOptions } from './Variables';
export function FilterDynamicComponent(props) { type Props = {
value: any;
onChange: (value: any) => void;
renderSchemaComponent: () => React.ReactNode;
collectionName: string;
};
export function FilterDynamicComponent(props: Props) {
const { value, onChange, renderSchemaComponent, collectionName } = props; const { value, onChange, renderSchemaComponent, collectionName } = props;
const scope = useVariableOptions(collectionName); const scope = useVariableOptions(collectionName);
return ( return (

View File

@ -25,6 +25,7 @@ import { useTranslation } from 'react-i18next';
import { import {
APIClientProvider, APIClientProvider,
ActionContext, ActionContext,
CollectionFieldOptions,
CollectionManagerContext, CollectionManagerContext,
Designable, Designable,
FormProvider, FormProvider,
@ -35,10 +36,10 @@ import {
findFormBlock, findFormBlock,
useAPIClient, useAPIClient,
useCollection, useCollection,
useLinkageCollectionFilterOptions,
useCollectionManager, useCollectionManager,
useCompile, useCompile,
useDesignable, useDesignable,
useLinkageCollectionFilterOptions,
} from '..'; } from '..';
import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks'; import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks';
import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils'; import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils';
@ -1127,3 +1128,21 @@ SchemaSettings.EnableChildCollections = function EnableChildCollections(props) {
/> />
); );
}; };
// 是否显示默认值配置项
export const isShowDefaultValue = (collectionField: CollectionFieldOptions, getInterface) => {
return (
!['o2o', 'oho', 'obo', 'o2m', 'attachment'].includes(collectionField?.interface) &&
!isSystemField(collectionField, getInterface)
);
};
// 是否是系统字段
export const isSystemField = (collectionField: CollectionFieldOptions, getInterface) => {
const i = getInterface?.(collectionField?.interface);
return i?.group === 'systemInfo';
};
export const isPatternDisabled = (fieldSchema: Schema) => {
return fieldSchema?.['x-component-props']?.['pattern-disable'] == true;
};

View File

@ -0,0 +1,44 @@
import React, { useMemo } from 'react';
import { Variable, useCompile } from '../../schema-component';
import { useUserVariable } from './hooks/useUserVariable';
type Props = {
value: any;
onChange: (value: any) => void;
collectionName: string;
renderSchemaComponent?: (props: any) => any;
style: React.CSSProperties;
schema: any;
operator: any;
children: any;
};
export const VariableInput = (props: Props) => {
const { value, onChange, renderSchemaComponent: RenderSchemaComponent, style, schema } = props;
const compile = useCompile();
const userVariable = useUserVariable({ schema, level: 1 });
const scope = useMemo(() => {
return [
userVariable,
compile({
label: `{{t("Date variables")}}`,
value: '$date',
key: '$date',
disabled: schema['x-component'] !== 'DatePicker',
children: [
{
key: 'now',
value: 'now',
label: `{{t("Now")}}`,
},
],
}),
];
}, []);
return (
<Variable.Input value={value} onChange={onChange} scope={scope} style={style}>
<RenderSchemaComponent value={value} onChange={onChange} />
</Variable.Input>
);
};

View File

@ -0,0 +1,152 @@
import { useCompile } from '../../../schema-component';
export const useDateVariable = ({ operator, schema }) => {
const compile = useCompile();
const operatorValue = operator?.value || '';
if (!operator || !schema) return null;
const disabled = !['DatePicker', 'DatePicker.RangePicker'].includes(schema['x-component']);
const dateOptions = [
{
key: 'now',
value: 'now',
label: `{{t("Now")}}`,
disabled: schema['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween',
},
{
key: 'yesterday',
value: 'yesterday',
label: `{{t("Yesterday")}}`,
disabled,
},
{
key: 'today',
value: 'today',
label: `{{t("Today")}}`,
disabled,
},
{
key: 'tomorrow',
value: 'tomorrow',
label: `{{t("Tomorrow")}}`,
disabled,
},
{
key: 'lastIsoWeek',
value: 'lastIsoWeek',
label: `{{t("Last week")}}`,
disabled,
},
{
key: 'thisIsoWeek',
value: 'thisIsoWeek',
label: `{{t("This week")}}`,
disabled,
},
{
key: 'nextIsoWeek',
value: 'nextIsoWeek',
label: `{{t("Next week")}}`,
disabled,
},
{
key: 'lastMonth',
value: 'lastMonth',
label: `{{t("Last month")}}`,
disabled,
},
{
key: 'thisMonth',
value: 'thisMonth',
label: `{{t("This month")}}`,
disabled,
},
{
key: 'nextMonth',
value: 'nextMonth',
label: `{{t("Next month")}}`,
disabled,
},
{
key: 'lastQuarter',
value: 'lastQuarter',
label: `{{t("Last quarter")}}`,
disabled,
},
{
key: 'thisQuarter',
value: 'thisQuarter',
label: `{{t("This quarter")}}`,
disabled,
},
{
key: 'nextQuarter',
value: 'nextQuarter',
label: `{{t("Next quarter")}}`,
disabled,
},
{
key: 'lastYear',
value: 'lastYear',
label: `{{t("Last year")}}`,
disabled,
},
{
key: 'thisYear',
value: 'thisYear',
label: `{{t("This year")}}`,
disabled,
},
{
key: 'nextYear',
value: 'nextYear',
label: `{{t("Next year")}}`,
disabled,
},
{
key: 'last7Days',
value: 'last7Days',
label: `{{t("Last 7 days")}}`,
disabled,
},
{
key: 'next7Days',
value: 'next7Days',
label: `{{t("Next 7 days")}}`,
disabled,
},
{
key: 'last30Days',
value: 'last30Days',
label: `{{t("Last 30 days")}}`,
disabled,
},
{
key: 'next30Days',
value: 'next30Days',
label: `{{t("Next 30 days")}}`,
disabled,
},
{
key: 'last90Days',
value: 'last90Days',
label: `{{t("Last 90 days")}}`,
disabled,
},
{
key: 'next90Days',
value: 'next90Days',
label: `{{t("Next 90 days")}}`,
disabled,
},
];
return compile({
label: `{{t("Date variables")}}`,
value: '$date',
key: '$date',
disabled: dateOptions.every((option) => option.disabled),
children: dateOptions,
});
};

View File

@ -0,0 +1,67 @@
import { useMemo } from 'react';
import { useCompile, useGetFilterOptions } from '../../../schema-component';
interface GetOptionsParams {
schema: any;
operator?: string;
maxDepth: number;
count?: number;
getFilterOptions: (collectionName: string) => any[];
}
const getChildren = (options: any[], { schema, operator, maxDepth, count = 1, getFilterOptions }: GetOptionsParams) => {
if (count > maxDepth) {
return [];
}
const result = options.map((option) => {
if ((option.type !== 'belongsTo' && option.type !== 'hasOne') || !option.target) {
return {
key: option.name,
value: option.name,
label: option.title,
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
disabled: schema?.['x-component'] !== option.schema?.['x-component'],
};
}
const children =
getChildren(getFilterOptions(option.target), {
schema,
operator,
maxDepth,
count: count + 1,
getFilterOptions,
}) || [];
return {
key: option.name,
value: option.name,
label: option.title,
children,
disabled: children.every((child) => child.disabled),
};
});
return result;
};
export const useUserVariable = ({ operator, schema, level }: { operator?: any; schema: any; level?: number }) => {
const compile = useCompile();
const getFilterOptions = useGetFilterOptions();
const children = useMemo(
() => getChildren(getFilterOptions('users'), { schema, operator, maxDepth: level || 3, getFilterOptions }) || [],
[operator, schema],
);
return useMemo(() => {
return compile({
label: `{{t("Current user")}}`,
value: '$user',
key: '$user',
disabled: children.every((option) => option.disabled),
children: children,
});
}, [children]);
};

View File

@ -0,0 +1,13 @@
import { useValues } from '../../../schema-component/antd/filter/useValues';
import { useDateVariable } from './useDateVariable';
import { useUserVariable } from './useUserVariable';
export const useVariableOptions = () => {
const { operator, schema } = useValues();
const userVariable = useUserVariable({ operator, schema });
const dateVariable = useDateVariable({ operator, schema });
if (!operator || !schema) return [];
return [userVariable, dateVariable];
};

View File

@ -3,10 +3,11 @@ import { ISchema, useField, useFieldSchema } from '@formily/react';
import { import {
GeneralSchemaDesigner, GeneralSchemaDesigner,
SchemaSettings, SchemaSettings,
isPatternDisabled,
useCollection, useCollection,
useCollectionManager, useCollectionManager,
useDesignable, useDesignable,
useFormBlockContext useFormBlockContext,
} from '@nocobase/client'; } from '@nocobase/client';
import set from 'lodash/set'; import set from 'lodash/set';
import React from 'react'; import React from 'react';
@ -155,7 +156,7 @@ const Designer = () => {
}} }}
/> />
)} )}
{form && !form?.readPretty && fieldSchema?.['x-component-props']?.['pattern-disable'] != true && ( {form && !form?.readPretty && !isPatternDisabled(fieldSchema) && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
key="pattern" key="pattern"
title={t('Pattern')} title={t('Pattern')}

View File

@ -26,7 +26,7 @@ const onTargetFieldChange = (field: Field) => {
!targetField.getState().disabled && targetField.setValue([]); !targetField.getState().disabled && targetField.setValue([]);
}; };
function makeFieldsPathOptions(fields, appends = []) { function MakeFieldsPathOptions(fields, appends = []) {
const { getCollection } = useCollectionManager(); const { getCollection } = useCollectionManager();
const options = []; const options = [];
fields.forEach((field) => { fields.forEach((field) => {
@ -41,7 +41,7 @@ function makeFieldsPathOptions(fields, appends = []) {
options.push({ options.push({
label: field.uiSchema?.title ?? field.name, label: field.uiSchema?.title ?? field.name,
value: field.name, value: field.name,
children: makeFieldsPathOptions(nextCollection.fields, nextAppends), children: MakeFieldsPathOptions(nextCollection.fields, nextAppends),
}); });
} }
} else { } else {
@ -120,7 +120,7 @@ export const snapshot: IField = {
const { getCollection } = useCollectionManager(); const { getCollection } = useCollectionManager();
const { fields } = getCollection(targetCollection); const { fields } = getCollection(targetCollection);
const result = makeFieldsPathOptions(fields, appends); const result = MakeFieldsPathOptions(fields, appends);
return [ return [
{ {