feat: customize action support create record for any collection (#2264)
* feat: customize button support customize add record * refactor: code improve * refactor: schemaSetting default value * refactor: schemaSetting default value * refactor: schemaSetting default value * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: locale improve * refactor: code improve * fix: fix style of default variable input (T-1154) * fix: merge bug --------- Co-authored-by: Rain <958414905@qq.com>
This commit is contained in:
parent
b42e3b4042
commit
18900d54f4
packages/core/client/src
block-provider
locale
schema-component
antd
__builtins__
action
association-field
form-item
table-v2
variable
common/utils
schema-initializer
buttons
items
schema-settings
@ -66,7 +66,7 @@ export const useIsEmptyRecord = () => {
|
||||
|
||||
export const FormBlockProvider = (props) => {
|
||||
const record = useRecord();
|
||||
const { collection } = props;
|
||||
const { collection, isCusomeizeCreate } = props;
|
||||
const { __collection } = record;
|
||||
const currentCollection = useCollection();
|
||||
const { designable } = useDesignable();
|
||||
@ -81,9 +81,9 @@ export const FormBlockProvider = (props) => {
|
||||
const createFlag =
|
||||
(currentCollection.name === (collection?.name || collection) && !isEmptyRecord) || !currentCollection.name;
|
||||
return (
|
||||
(detailFlag || createFlag) && (
|
||||
<BlockProvider {...props} block={'form'} params={{ ...props?.params, targetCollection: collection }}>
|
||||
<InternalFormBlockProvider {...props} params={{ ...props?.params, targetCollection: collection }} />
|
||||
(detailFlag || createFlag || isCusomeizeCreate) && (
|
||||
<BlockProvider {...props} block={'form'}>
|
||||
<InternalFormBlockProvider {...props} />
|
||||
</BlockProvider>
|
||||
)
|
||||
);
|
||||
|
@ -3,10 +3,10 @@ import { FormContext, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useCollectionManager } from '../collection-manager';
|
||||
import { useFilterBlock } from '../filter-provider/FilterProvider';
|
||||
import { FixedBlockWrapper, removeNullCondition, SchemaComponentOptions } from '../schema-component';
|
||||
import { FixedBlockWrapper, SchemaComponentOptions, removeNullCondition } from '../schema-component';
|
||||
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
|
||||
import { findFilterTargets } from './hooks';
|
||||
import { mergeFilter } from './SharedFilterProvider';
|
||||
import { findFilterTargets } from './hooks';
|
||||
|
||||
export const TableBlockContext = createContext<any>({});
|
||||
export function getIdsWithChildren(nodes) {
|
||||
@ -31,7 +31,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const InternalTableBlockProvider = (props: Props) => {
|
||||
const { params, showIndex, dragSort, rowKey, childrenColumnName, fieldNames } = props;
|
||||
const { params, showIndex, dragSort, rowKey, childrenColumnName, fieldNames, ...others } = props;
|
||||
const field: any = useField();
|
||||
const { resource, service } = useBlockRequestContext();
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -47,6 +47,7 @@ const InternalTableBlockProvider = (props: Props) => {
|
||||
<FixedBlockWrapper>
|
||||
<TableBlockContext.Provider
|
||||
value={{
|
||||
...others,
|
||||
field,
|
||||
service,
|
||||
resource,
|
||||
@ -97,7 +98,11 @@ export const TableBlockProvider = (props) => {
|
||||
<SchemaComponentOptions scope={{ treeTable }}>
|
||||
<FormContext.Provider value={form}>
|
||||
<BlockProvider {...props} params={params} runWhenParamsChanged>
|
||||
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} />
|
||||
<InternalTableBlockProvider
|
||||
{...props}
|
||||
childrenColumnName={childrenColumnName}
|
||||
params={params}
|
||||
/>
|
||||
</BlockProvider>
|
||||
</FormContext.Provider>
|
||||
</SchemaComponentOptions>
|
||||
@ -117,7 +122,7 @@ export const useTableBlockProps = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!ctx?.service?.loading) {
|
||||
field.value=[];
|
||||
field.value = [];
|
||||
field.value = ctx?.service?.data?.data;
|
||||
field.data = field.data || {};
|
||||
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
|
||||
|
@ -710,4 +710,5 @@ export default {
|
||||
"Allow add new, update and delete actions":"Allow add new, update and delete actions",
|
||||
"Date display format":"Date display format",
|
||||
"Assign data scope for the template":"Assign data scope for the template",
|
||||
"Table selected records":"Table selected records"
|
||||
};
|
||||
|
@ -795,4 +795,5 @@ export default {
|
||||
"Allow add new, update and delete actions":"允许增删改操作",
|
||||
"Date display format":"日期显示格式",
|
||||
"Assign data scope for the template":"为模板指定数据范围",
|
||||
"Table selected records":"表格中选中的记录"
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ export type UseComponentStyleResult = {
|
||||
wrapSSR: ReturnType<typeof useStyleRegister>;
|
||||
hashId: string;
|
||||
componentCls: string;
|
||||
rootPrefixCls: string;
|
||||
};
|
||||
|
||||
export const genStyleHook = <ComponentName extends OverrideComponent>(
|
||||
@ -99,6 +100,7 @@ export const genStyleHook = <ComponentName extends OverrideComponent>(
|
||||
),
|
||||
hashId,
|
||||
componentCls: prefixCls,
|
||||
rootPrefixCls,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -580,7 +580,7 @@ export const ActionDesigner = (props) => {
|
||||
const { name } = useCollection();
|
||||
const { getChildrenCollections } = useCollectionManager();
|
||||
const isAction = useLinkageAction();
|
||||
const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate'].includes(
|
||||
const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate','customize:create'].includes(
|
||||
fieldSchema['x-action'] || '',
|
||||
);
|
||||
const isUpdateModePopupAction = ['customize:bulkUpdate', 'customize:bulkEdit'].includes(fieldSchema['x-action']);
|
||||
|
@ -4,7 +4,8 @@ import { Space, message } from 'antd';
|
||||
import { isFunction } from 'mathjs';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RecordProvider, useAPIClient, useCollectionManager } from '../../../';
|
||||
import { RecordProvider, useAPIClient } from '../../../';
|
||||
import { isVariable } from '../../common/utils/uitls';
|
||||
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
|
||||
import useServiceOptions, { useAssociationFieldContext } from './hooks';
|
||||
|
||||
@ -17,10 +18,10 @@ const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
|
||||
const { objectValue = true } = props;
|
||||
const field: any = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { getCollection } = useCollectionManager();
|
||||
const service = useServiceOptions(props);
|
||||
const { options: collectionField } = useAssociationFieldContext();
|
||||
const value = Array.isArray(props.value) ? props.value.filter(Boolean) : props.value;
|
||||
const initValue = isVariable(props.value) ? undefined : props.value;
|
||||
const value = Array.isArray(initValue) ? initValue.filter(Boolean) : initValue;
|
||||
const addMode = fieldSchema['x-component-props']?.addMode;
|
||||
const isAllowAddNew = fieldSchema['x-add-new'];
|
||||
const { t } = useTranslation();
|
||||
@ -28,7 +29,6 @@ const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const resource = api.resource(collectionField.target);
|
||||
const targetCollection = getCollection(collectionField.target);
|
||||
const handleCreateAction = async (props) => {
|
||||
const { search: value, callBack } = props;
|
||||
const {
|
||||
|
@ -22,9 +22,7 @@ import { isTitleField } from '../../../collection-manager/Configuration/Collecti
|
||||
import { GeneralSchemaItems } from '../../../schema-items/GeneralSchemaItems';
|
||||
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';
|
||||
import { BlockItem } from '../block-item';
|
||||
import { removeNullCondition } from '../filter';
|
||||
@ -33,29 +31,72 @@ import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
import { FilterFormDesigner } from './FormItem.FilterFormDesigner';
|
||||
import { useEnsureOperatorsValid } from './SchemaSettingOptions';
|
||||
|
||||
const defaultInputStyle = css`
|
||||
& > .nb-form-item {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
export const findColumnFieldSchema = (fieldSchema, getCollectionJoinField) => {
|
||||
const childsSchema = new Set();
|
||||
const getAssociationAppends = (schema) => {
|
||||
schema.reduceProperties((_, s) => {
|
||||
const collectionfield = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field']);
|
||||
const isAssociationField = collectionfield && ['belongsTo'].includes(collectionfield.type);
|
||||
if (collectionfield && isAssociationField && s.default?.includes?.('$context')) {
|
||||
childsSchema.add(JSON.stringify({ name: s.name, default: s.default }));
|
||||
} else {
|
||||
getAssociationAppends(s);
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
getAssociationAppends(fieldSchema);
|
||||
return [...childsSchema];
|
||||
};
|
||||
|
||||
export const FormItem: any = observer(
|
||||
(props: any) => {
|
||||
useEnsureOperatorsValid();
|
||||
|
||||
const field = useField<Field>();
|
||||
const ctx = useBlockRequestContext();
|
||||
const schema = useFieldSchema();
|
||||
const variablesCtx = useVariablesCtx();
|
||||
|
||||
const { getCollectionJoinField } = useCollectionManager();
|
||||
const collectionField = getCollectionJoinField(schema['x-collection-field']);
|
||||
useEffect(() => {
|
||||
if (ctx?.block === 'form') {
|
||||
ctx.field.data = ctx.field.data || {};
|
||||
ctx.field.data.activeFields = ctx.field.data.activeFields || new Set();
|
||||
ctx.field.data.activeFields.add(schema.name);
|
||||
// 如果默认值是一个变量,则需要解析之后再显示出来
|
||||
if (isVariable(schema?.default)) {
|
||||
if (isVariable(schema?.default) && !schema?.default.includes('$context')) {
|
||||
field.setInitialValue?.(parseVariables(schema.default, variablesCtx));
|
||||
} else if (
|
||||
isVariable(schema?.default) &&
|
||||
schema?.default?.includes('$context') &&
|
||||
collectionField.interface === 'm2m'
|
||||
) {
|
||||
// 直接对多
|
||||
const contextData = parseVariables('{{$context}}', variablesCtx);
|
||||
let iniValues = [];
|
||||
contextData?.map((v) => {
|
||||
const data = parseVariables(schema.default, { $context: v });
|
||||
iniValues = iniValues.concat(data);
|
||||
});
|
||||
field.setInitialValue?.(_.uniqBy(iniValues, 'id'));
|
||||
} else if (
|
||||
collectionField?.interface === 'o2m' &&
|
||||
['SubTable', 'Nester'].includes(schema?.['x-component-props']?.['mode']) // 间接对多
|
||||
) {
|
||||
const childrenFieldWithDefault = findColumnFieldSchema(schema, getCollectionJoinField);
|
||||
// 子表格/子表单中找出所有belongsTo字段的上下文默认值
|
||||
if (childrenFieldWithDefault.length > 0) {
|
||||
const contextData = parseVariables('{{$context}}', variablesCtx);
|
||||
const initValues = contextData?.map((v) => {
|
||||
const obj = {};
|
||||
childrenFieldWithDefault.forEach((s: any) => {
|
||||
const child = JSON.parse(s);
|
||||
obj[child.name] = parseVariables(child.default, { $context: v });
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
field.setInitialValue?.(initValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
@ -109,7 +150,6 @@ FormItem.Designer = function Designer() {
|
||||
const { t } = useTranslation();
|
||||
const { dn, refresh, insertAdjacent } = useDesignable();
|
||||
const compile = useCompile();
|
||||
const variablesCtx = useVariablesCtx();
|
||||
const IsShowMultipleSwitch = useIsShowMultipleSwitch();
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
if (collectionField?.target) {
|
||||
@ -168,9 +208,6 @@ FormItem.Designer = function Designer() {
|
||||
direction: 'asc',
|
||||
};
|
||||
});
|
||||
|
||||
const fieldSchemaWithoutRequired = _.omit(fieldSchema, 'required');
|
||||
|
||||
const isSubFormMode = fieldSchema['x-component-props']?.mode === 'Nester';
|
||||
const isPickerMode = fieldSchema['x-component-props']?.mode === 'Picker';
|
||||
const showFieldMode = isAssociationField && fieldModeOptions && !isTableField;
|
||||
@ -345,76 +382,7 @@ FormItem.Designer = function Designer() {
|
||||
{form &&
|
||||
!form?.readPretty &&
|
||||
isShowDefaultValue(collectionField, getInterface) &&
|
||||
!isPatternDisabled(fieldSchema) && (
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set default value')}
|
||||
components={{ ArrayCollapse, FormLayout, VariableInput }}
|
||||
width={800}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Set default value'),
|
||||
properties: {
|
||||
default: {
|
||||
...(fieldSchemaWithoutRequired || {}),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'VariableInput',
|
||||
'x-component-props': {
|
||||
...(fieldSchema?.['x-component-props'] || {}),
|
||||
collectionField,
|
||||
targetField,
|
||||
collectionName: collectionField?.collectionName,
|
||||
schema: collectionField?.uiSchema,
|
||||
className: defaultInputStyle,
|
||||
renderSchemaComponent: function Com(props) {
|
||||
const s = _.cloneDeep(fieldSchemaWithoutRequired) || ({} as Schema);
|
||||
|
||||
s.title = '';
|
||||
s['x-read-pretty'] = false;
|
||||
s['x-disabled'] = false;
|
||||
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
...(s || {}),
|
||||
'x-component-props': {
|
||||
...s['x-component-props'],
|
||||
onChange: props.onChange,
|
||||
value: props.value,
|
||||
defaultValue: getFieldDefaultValue(s, collectionField),
|
||||
style: {
|
||||
width: '100%',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
name: 'default',
|
||||
title: t('Default value'),
|
||||
default: getFieldDefaultValue(fieldSchema, collectionField),
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={(v) => {
|
||||
const schema: ISchema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
if (field.value !== v.default) {
|
||||
field.value = parseVariables(v.default, variablesCtx);
|
||||
}
|
||||
fieldSchema.default = v.default;
|
||||
schema.default = v.default;
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
!isPatternDisabled(fieldSchema) && <SchemaSettings.DefaultValue />}
|
||||
{isSelectFieldMode && !field.readPretty && (
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set the data scope')}
|
||||
|
@ -3,7 +3,7 @@ import { set } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollectionFilterOptions, useCollectionManager } from '../../../collection-manager';
|
||||
import { GeneralSchemaDesigner, isPatternDisabled, SchemaSettings } from '../../../schema-settings';
|
||||
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
|
||||
import { useCompile, useDesignable } from '../../hooks';
|
||||
import { useAssociationFieldContext } from '../association-field/hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
@ -326,6 +326,10 @@ export const TableColumnDesigner = (props) => {
|
||||
)}
|
||||
{isDateField && <SchemaSettings.DataFormat fieldSchema={fieldSchema} />}
|
||||
|
||||
|
||||
{isSubTableColumn && !field?.readPretty && isShowDefaultValue(collectionField, getInterface) && (
|
||||
<SchemaSettings.DefaultValue fieldSchema={fieldSchema}/>
|
||||
)}
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren={!isSubTableColumn}
|
||||
|
@ -3,6 +3,7 @@ import { css, cx } from '@emotion/css';
|
||||
import { useForm } from '@formily/react';
|
||||
import { dayjs, error } from '@nocobase/utils/client';
|
||||
import { Input as AntInput, Cascader, DatePicker, InputNumber, Select, Space, Tag } from 'antd';
|
||||
import useAntdInputStyle from 'antd/es/input/style';
|
||||
import type { DefaultOptionType } from 'antd/lib/cascader';
|
||||
import classNames from 'classnames';
|
||||
import { cloneDeep } from 'lodash';
|
||||
@ -155,7 +156,11 @@ export function Input(props) {
|
||||
changeOnSelect,
|
||||
fieldNames,
|
||||
} = props;
|
||||
const { wrapSSR, hashId, componentCls } = useStyles();
|
||||
const { wrapSSR, hashId, componentCls, rootPrefixCls } = useStyles();
|
||||
|
||||
// 添加 antd input 样式,防止样式缺失
|
||||
useAntdInputStyle(`${rootPrefixCls}-input`);
|
||||
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const form = useForm();
|
||||
|
@ -8,9 +8,6 @@ export const useStyles = genStyleHook('nb-variable', (token) => {
|
||||
const tagFontSize = token.fontSizeSM;
|
||||
const tagLineHeight = `${token.lineHeightSM * tagFontSize}px`;
|
||||
const defaultBg = colorFillQuaternary;
|
||||
const lightColor = token[`blue1`];
|
||||
const lightBorderColor = token[`blue3`];
|
||||
const textColor = token[`blue7`];
|
||||
|
||||
return {
|
||||
[componentCls]: {
|
||||
|
@ -2,6 +2,7 @@ import { dayjs } from '@nocobase/utils/client';
|
||||
import flat from 'flat';
|
||||
import _, { every, findIndex, isArray, some } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { useTableBlockContext } from '../../../block-provider';
|
||||
import { useCurrentUserContext } from '../../../user';
|
||||
import jsonLogic from '../../common/utils/logic';
|
||||
|
||||
@ -14,12 +15,15 @@ type VariablesCtx = {
|
||||
|
||||
export const useVariablesCtx = (): VariablesCtx => {
|
||||
const { data } = useCurrentUserContext() || {};
|
||||
const { field, service, rowKey } = useTableBlockContext();
|
||||
const contextData = service?.data?.data?.filter((v) => (field?.data?.selectedRowKeys || [])?.includes(v[rowKey]));
|
||||
return useMemo(() => {
|
||||
return {
|
||||
$user: data?.data || {},
|
||||
$date: {
|
||||
now: () => dayjs().toISOString(),
|
||||
},
|
||||
$context: contextData,
|
||||
};
|
||||
}, [data]);
|
||||
};
|
||||
@ -33,7 +37,7 @@ export const isVariable = (str: unknown) => {
|
||||
return matches ? true : false;
|
||||
};
|
||||
|
||||
export const parseVariables = (str: string, ctx: VariablesCtx) => {
|
||||
export const parseVariables = (str: string, ctx: VariablesCtx | any) => {
|
||||
const regex = /{{(.*?)}}/;
|
||||
const matches = str?.match?.(regex);
|
||||
if (matches) {
|
||||
|
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaInitializer } from '../..';
|
||||
import { gridRowColWrap } from '../utils';
|
||||
|
||||
export const CusomeizeCreateFormBlockInitializers = (props: any) => {
|
||||
const { t } = useTranslation();
|
||||
const { insertPosition, component } = props;
|
||||
return (
|
||||
<SchemaInitializer.Button
|
||||
wrap={gridRowColWrap}
|
||||
title={component ? null : t('Add block')}
|
||||
icon={'PlusOutlined'}
|
||||
insertPosition={insertPosition}
|
||||
component={component}
|
||||
items={[
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Data blocks")}}',
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Form")}}',
|
||||
component: 'FormBlockInitializer',
|
||||
isCusomeizeCreate: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Other blocks")}}',
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Markdown")}}',
|
||||
component: 'MarkdownBlockInitializer',
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
@ -156,6 +156,19 @@ export const TableActionInitializers = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Add record")}}',
|
||||
component: 'CustomizeAddRecordActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action': 'create',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
|
@ -4,6 +4,7 @@ export * from './CalendarActionInitializers';
|
||||
export * from './CalendarFormActionInitializers';
|
||||
export * from './CreateFormBlockInitializers';
|
||||
export * from './CreateFormBulkEditBlockInitializers';
|
||||
export * from './CusomeizeCreateFormBlockInitializers';
|
||||
export * from './CustomFormItemInitializers';
|
||||
export * from './DetailsActionInitializers';
|
||||
export * from './FilterFormActionInitializers';
|
||||
@ -25,4 +26,3 @@ export * from './TableColumnInitializers';
|
||||
export * from './TableSelectorInitializers';
|
||||
// association filter
|
||||
export * from '../../schema-component/antd/association-filter/AssociationFilter';
|
||||
|
||||
|
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { BlockInitializer } from './BlockInitializer';
|
||||
|
||||
export const CustomizeAddRecordActionInitializer = (props) => {
|
||||
const schema = {
|
||||
type: 'void',
|
||||
title: '{{t("Add record")}}',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-component': 'Action',
|
||||
'x-action': 'customize:create',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
icon: 'PlusOutlined',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
title: '{{t("Add record")}}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
properties: {
|
||||
tabs: {
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'TabPaneInitializersForCreateFormBlock',
|
||||
properties: {
|
||||
tab1: {
|
||||
type: 'void',
|
||||
title: '{{t("Add record")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'CusomeizeCreateFormBlockInitializers',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return <BlockInitializer {...props} schema={schema} />;
|
||||
};
|
@ -6,10 +6,10 @@ import { useSchemaTemplateManager } from '../../schema-templates';
|
||||
import { useCollectionDataSourceItems } from '../utils';
|
||||
|
||||
export const DataBlockInitializer = (props) => {
|
||||
const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props;
|
||||
const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, insert, isCusomeizeCreate, ...others } =
|
||||
props;
|
||||
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||
const { setVisible } = useContext(SchemaInitializerButtonContext);
|
||||
|
||||
return (
|
||||
<SchemaInitializer.Item
|
||||
icon={<TableOutlined />}
|
||||
@ -22,7 +22,7 @@ export const DataBlockInitializer = (props) => {
|
||||
if (onCreateBlockSchema) {
|
||||
onCreateBlockSchema({ item });
|
||||
} else if (createBlockSchema) {
|
||||
insert(createBlockSchema({ collection: item.name }));
|
||||
insert(createBlockSchema({ collection: item.name, isCusomeizeCreate }));
|
||||
}
|
||||
}
|
||||
setVisible(false);
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { FormOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
import { createFormBlockSchema } from '../utils';
|
||||
import { DataBlockInitializer } from './DataBlockInitializer';
|
||||
|
||||
export const FormBlockInitializer = (props) => {
|
||||
const { isCusomeizeCreate } = props;
|
||||
return (
|
||||
<DataBlockInitializer
|
||||
{...props}
|
||||
@ -11,6 +12,7 @@ export const FormBlockInitializer = (props) => {
|
||||
componentType={'FormItem'}
|
||||
templateWrap={(templateSchema, { item }) => {
|
||||
const s = createFormBlockSchema({
|
||||
isCusomeizeCreate,
|
||||
template: templateSchema,
|
||||
collection: item.name,
|
||||
});
|
||||
|
@ -17,6 +17,7 @@ export * from './CreateFormBulkEditBlockInitializer';
|
||||
export * from './CreateResetActionInitializer';
|
||||
export * from './CreateSubmitActionInitializer';
|
||||
export * from './CustomizeActionInitializer';
|
||||
export * from './CustomizeAddRecordActionInitializer';
|
||||
export * from './CustomizeBulkEditActionInitializer';
|
||||
export * from './DataBlockInitializer';
|
||||
export * from './DeleteEventActionInitializer';
|
||||
@ -53,4 +54,3 @@ export * from './TableSelectorInitializer';
|
||||
export * from './UpdateActionInitializer';
|
||||
export * from './UpdateSubmitActionInitializer';
|
||||
export * from './ViewActionInitializer';
|
||||
|
||||
|
@ -57,10 +57,13 @@ import {
|
||||
useGlobalTheme,
|
||||
useLinkageCollectionFilterOptions,
|
||||
} from '..';
|
||||
import { useTableBlockContext } from '../block-provider';
|
||||
import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks';
|
||||
import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils';
|
||||
import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem';
|
||||
import { getTargetKey } from '../schema-component/antd/association-filter/utilts';
|
||||
import { getFieldDefaultValue } from '../schema-component/antd/form-item';
|
||||
import { parseVariables, useVariablesCtx } from '../schema-component/common/utils/uitls';
|
||||
import { useSchemaTemplateManager } from '../schema-templates';
|
||||
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
|
||||
import { FormDataTemplates } from './DataTemplates';
|
||||
@ -69,6 +72,7 @@ import { EnableChildCollections } from './EnableChildCollections';
|
||||
import { ChildDynamicComponent } from './EnableChildCollections/DynamicComponent';
|
||||
import { FormLinkageRules } from './LinkageRules';
|
||||
import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks';
|
||||
import { VariableInput } from './VariableInput/VariableInput';
|
||||
|
||||
interface SchemaSettingsProps {
|
||||
title?: any;
|
||||
@ -1414,6 +1418,118 @@ SchemaSettings.DataFormat = function DateFormatConfig(props: { fieldSchema: Sche
|
||||
);
|
||||
};
|
||||
|
||||
const defaultInputStyle = css`
|
||||
& > .nb-form-item {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const findParentFieldSchema = (fieldSchema: Schema) => {
|
||||
let parent = fieldSchema.parent;
|
||||
while (parent) {
|
||||
if (parent['x-component'] === 'CollectionField') {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
};
|
||||
|
||||
SchemaSettings.DefaultValue = function DefaultvalueConfigure(props) {
|
||||
const variablesCtx = useVariablesCtx();
|
||||
const currentSchema = useFieldSchema();
|
||||
const fieldSchema = props?.fieldSchema ?? currentSchema;
|
||||
const field = useField<Field>();
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
let targetField;
|
||||
const { getField } = useCollection();
|
||||
const { getCollectionJoinField } = useCollectionManager();
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
const fieldSchemaWithoutRequired = _.omit(fieldSchema, 'required');
|
||||
if (collectionField?.target) {
|
||||
targetField = getCollectionJoinField(
|
||||
`${collectionField.target}.${fieldSchema['x-component-props']?.fieldNames?.label || 'id'}`,
|
||||
);
|
||||
}
|
||||
const parentFieldSchema = collectionField.interface === 'm2o' && findParentFieldSchema(fieldSchema);
|
||||
const parentCollectionField = parentFieldSchema && getCollectionJoinField(parentFieldSchema?.['x-collection-field']);
|
||||
const tableCtx = useTableBlockContext();
|
||||
const isAllowContexVariable =
|
||||
collectionField.interface === 'm2m' ||
|
||||
(parentCollectionField?.type === 'hasMany' && collectionField.interface === 'm2o');
|
||||
return (
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set default value')}
|
||||
components={{ ArrayCollapse, FormLayout, VariableInput }}
|
||||
width={800}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Set default value'),
|
||||
properties: {
|
||||
default: {
|
||||
...(fieldSchemaWithoutRequired || {}),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'VariableInput',
|
||||
'x-component-props': {
|
||||
...(fieldSchema?.['x-component-props'] || {}),
|
||||
collectionField,
|
||||
targetField,
|
||||
collectionName: collectionField?.collectionName,
|
||||
contextCollectionName: isAllowContexVariable && tableCtx.collection,
|
||||
schema: collectionField?.uiSchema,
|
||||
className: defaultInputStyle,
|
||||
renderSchemaComponent: function Com(props) {
|
||||
const s = _.cloneDeep(fieldSchemaWithoutRequired) || ({} as Schema);
|
||||
s.title = '';
|
||||
s['x-read-pretty'] = false;
|
||||
s['x-disabled'] = false;
|
||||
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
...(s || {}),
|
||||
'x-component-props': {
|
||||
...s['x-component-props'],
|
||||
onChange: props.onChange,
|
||||
value: props.value,
|
||||
defaultValue: getFieldDefaultValue(s, collectionField),
|
||||
style: {
|
||||
width: '100%',
|
||||
verticalAlign: 'top',
|
||||
minWidth: '200px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
name: 'default',
|
||||
title: t('Default value'),
|
||||
default: getFieldDefaultValue(fieldSchema, collectionField),
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={(v) => {
|
||||
const schema: ISchema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
if (field.value !== v.default) {
|
||||
field.value = parseVariables(v.default, variablesCtx);
|
||||
}
|
||||
fieldSchema.default = v.default;
|
||||
schema.default = v.default;
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
currentSchema,
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
// 是否显示默认值配置项
|
||||
export const isShowDefaultValue = (collectionField: CollectionFieldOptions, getInterface) => {
|
||||
return (
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { CollectionFieldOptions } from '../../collection-manager';
|
||||
import { useCompile, Variable } from '../../schema-component';
|
||||
import { useContextAssociationFields } from './hooks/useContextAssociationFields';
|
||||
import { useUserVariable } from './hooks/useUserVariable';
|
||||
|
||||
type Props = {
|
||||
@ -14,6 +15,7 @@ type Props = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
collectionField?: CollectionFieldOptions;
|
||||
contextCollectionName?: string;
|
||||
};
|
||||
|
||||
export const VariableInput = (props: Props) => {
|
||||
@ -25,9 +27,11 @@ export const VariableInput = (props: Props) => {
|
||||
schema,
|
||||
className,
|
||||
collectionField,
|
||||
contextCollectionName,
|
||||
} = props;
|
||||
const compile = useCompile();
|
||||
const userVariable = useUserVariable({ schema, maxDepth: 1 });
|
||||
const contextVariable = useContextAssociationFields({ schema, maxDepth: 2, contextCollectionName });
|
||||
const scope = useMemo(() => {
|
||||
const data = [
|
||||
compile({
|
||||
@ -47,11 +51,21 @@ export const VariableInput = (props: Props) => {
|
||||
if (collectionField?.target === 'users') {
|
||||
data.unshift(userVariable);
|
||||
}
|
||||
if (contextCollectionName) {
|
||||
data.unshift(contextVariable);
|
||||
}
|
||||
return data;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Variable.Input className={className} value={value} onChange={onChange} scope={scope} style={style}>
|
||||
<Variable.Input
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
scope={scope}
|
||||
style={style}
|
||||
changeOnSelect={contextCollectionName!==null}
|
||||
>
|
||||
<RenderSchemaComponent value={value} onChange={onChange} />
|
||||
</Variable.Input>
|
||||
);
|
||||
|
@ -0,0 +1,121 @@
|
||||
import { error } from '@nocobase/utils/client';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile, useGetFilterOptions } from '../../../schema-component';
|
||||
import { FieldOption, Option } from '../type';
|
||||
|
||||
interface GetOptionsParams {
|
||||
schema: any;
|
||||
depth: number;
|
||||
maxDepth?: number;
|
||||
loadChildren?: (option: Option) => Promise<void>;
|
||||
compile: (value: string) => any;
|
||||
}
|
||||
|
||||
const getChildren = (
|
||||
options: FieldOption[],
|
||||
{ schema, depth, maxDepth, loadChildren, compile }: GetOptionsParams,
|
||||
): Option[] => {
|
||||
const result = options
|
||||
.map((option): Option => {
|
||||
if (!option.target) {
|
||||
return {
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
label: compile(option.title),
|
||||
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
|
||||
// disabled: schema?.['x-component'] !== option.schema?.['x-component'],
|
||||
isLeaf: true,
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
if (depth >= maxDepth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
label: compile(option.title),
|
||||
isLeaf: true,
|
||||
field: option,
|
||||
depth,
|
||||
loadChildren,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useContextAssociationFields = ({
|
||||
schema,
|
||||
maxDepth = 3,
|
||||
contextCollectionName,
|
||||
}: {
|
||||
schema: any;
|
||||
maxDepth?: number;
|
||||
contextCollectionName: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const compile = useCompile();
|
||||
const getFilterOptions = useGetFilterOptions();
|
||||
|
||||
const loadChildren = (option: Option): Promise<void> => {
|
||||
if (!option.field?.target) {
|
||||
return new Promise((resolve) => {
|
||||
error('Must be set field target');
|
||||
option.children = [];
|
||||
resolve(void 0);
|
||||
});
|
||||
}
|
||||
|
||||
const collectionName = option.field.target;
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const children =
|
||||
getChildren(
|
||||
getFilterOptions(collectionName).filter((v) => {
|
||||
const isAssociationField = ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(v.type);
|
||||
return isAssociationField;
|
||||
}),
|
||||
{
|
||||
schema,
|
||||
depth: option.depth + 1,
|
||||
maxDepth,
|
||||
loadChildren,
|
||||
compile,
|
||||
},
|
||||
) || [];
|
||||
|
||||
if (children.length === 0) {
|
||||
option.disabled = true;
|
||||
option.children = [];
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
option.children = children;
|
||||
resolve();
|
||||
|
||||
// 延迟 5 毫秒,防止阻塞主线程,导致 UI 卡顿
|
||||
}, 5);
|
||||
});
|
||||
};
|
||||
|
||||
const result = useMemo(() => {
|
||||
return {
|
||||
label: t('Table selected records'),
|
||||
value: '$context',
|
||||
key: '$context',
|
||||
isLeaf: false,
|
||||
field: {
|
||||
target: contextCollectionName,
|
||||
},
|
||||
depth: 0,
|
||||
loadChildren,
|
||||
} as Option;
|
||||
}, [schema?.['x-component']]);
|
||||
|
||||
return result;
|
||||
};
|
Loading…
Reference in New Issue
Block a user