From 18900d54f48b16af70a60cb657766d0ef6e9eaeb Mon Sep 17 00:00:00 2001 From: katherinehhh Date: Tue, 25 Jul 2023 14:51:45 +0800 Subject: [PATCH] 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> --- .../src/block-provider/FormBlockProvider.tsx | 8 +- .../src/block-provider/TableBlockProvider.tsx | 15 +- packages/core/client/src/locale/en_US.ts | 1 + packages/core/client/src/locale/zh_CN.ts | 1 + .../antd/__builtins__/style.ts | 2 + .../antd/action/Action.Designer.tsx | 2 +- .../association-field/AssociationSelect.tsx | 8 +- .../antd/form-item/FormItem.tsx | 136 +++++++----------- .../antd/table-v2/Table.Column.Designer.tsx | 6 +- .../schema-component/antd/variable/Input.tsx | 7 +- .../schema-component/antd/variable/style.ts | 3 - .../schema-component/common/utils/uitls.tsx | 6 +- .../CusomeizeCreateFormBlockInitializers.tsx | 43 ++++++ .../buttons/TableActionInitializers.tsx | 13 ++ .../src/schema-initializer/buttons/index.ts | 2 +- .../CustomizeAddRecordActionInitializer.tsx | 52 +++++++ .../items/DataBlockInitializer.tsx | 6 +- .../items/FormBlockInitializer.tsx | 4 +- .../src/schema-initializer/items/index.tsx | 2 +- .../src/schema-settings/SchemaSettings.tsx | 116 +++++++++++++++ .../VariableInput/VariableInput.tsx | 16 ++- .../hooks/useContextAssociationFields.tsx | 121 ++++++++++++++++ 22 files changed, 459 insertions(+), 111 deletions(-) create mode 100644 packages/core/client/src/schema-initializer/buttons/CusomeizeCreateFormBlockInitializers.tsx create mode 100644 packages/core/client/src/schema-initializer/items/CustomizeAddRecordActionInitializer.tsx create mode 100644 packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx diff --git a/packages/core/client/src/block-provider/FormBlockProvider.tsx b/packages/core/client/src/block-provider/FormBlockProvider.tsx index ff4f5ae64..2245a3d20 100644 --- a/packages/core/client/src/block-provider/FormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FormBlockProvider.tsx @@ -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) && ( - - + (detailFlag || createFlag || isCusomeizeCreate) && ( + + ) ); diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index 181ba8a57..013c95dcf 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -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({}); 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) => { { - + @@ -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; diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index d7a2d3153..ad83395bb 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -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" }; diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 2e285cc18..73e2c4f5e 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -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":"表格中选中的记录" } diff --git a/packages/core/client/src/schema-component/antd/__builtins__/style.ts b/packages/core/client/src/schema-component/antd/__builtins__/style.ts index b8e9efc1d..2a5519562 100644 --- a/packages/core/client/src/schema-component/antd/__builtins__/style.ts +++ b/packages/core/client/src/schema-component/antd/__builtins__/style.ts @@ -59,6 +59,7 @@ export type UseComponentStyleResult = { wrapSSR: ReturnType; hashId: string; componentCls: string; + rootPrefixCls: string; }; export const genStyleHook = ( @@ -99,6 +100,7 @@ export const genStyleHook = ( ), hashId, componentCls: prefixCls, + rootPrefixCls, }; }; }; diff --git a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx index 343ea14d6..25df35327 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx @@ -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']); diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx index 2803d4c69..af9990550 100644 --- a/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx @@ -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 { diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx index 3b1ad0a9b..3607e887b 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx @@ -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(); 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) && ( - - ); - }, - }, - 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) && } {isSelectFieldMode && !field.readPretty && ( { )} {isDateField && } + + {isSubTableColumn && !field?.readPretty && isShowDefaultValue(collectionField, getInterface) && ( + + )} { 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]: { diff --git a/packages/core/client/src/schema-component/common/utils/uitls.tsx b/packages/core/client/src/schema-component/common/utils/uitls.tsx index 392ef7c08..fcf584efd 100644 --- a/packages/core/client/src/schema-component/common/utils/uitls.tsx +++ b/packages/core/client/src/schema-component/common/utils/uitls.tsx @@ -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) { diff --git a/packages/core/client/src/schema-initializer/buttons/CusomeizeCreateFormBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/CusomeizeCreateFormBlockInitializers.tsx new file mode 100644 index 000000000..bd5cb4dc8 --- /dev/null +++ b/packages/core/client/src/schema-initializer/buttons/CusomeizeCreateFormBlockInitializers.tsx @@ -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 ( + + ); +}; diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx index 8d7a0ff8d..4ba1e4e85 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx @@ -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(); diff --git a/packages/core/client/src/schema-initializer/buttons/index.ts b/packages/core/client/src/schema-initializer/buttons/index.ts index a8b747449..a02b9ce46 100644 --- a/packages/core/client/src/schema-initializer/buttons/index.ts +++ b/packages/core/client/src/schema-initializer/buttons/index.ts @@ -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'; - diff --git a/packages/core/client/src/schema-initializer/items/CustomizeAddRecordActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/CustomizeAddRecordActionInitializer.tsx new file mode 100644 index 000000000..4c00836dd --- /dev/null +++ b/packages/core/client/src/schema-initializer/items/CustomizeAddRecordActionInitializer.tsx @@ -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 ; +}; diff --git a/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx index 68fdad2e1..baa2624e4 100644 --- a/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx @@ -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 ( } @@ -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); diff --git a/packages/core/client/src/schema-initializer/items/FormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/FormBlockInitializer.tsx index 05b33d2f2..36a30849b 100644 --- a/packages/core/client/src/schema-initializer/items/FormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/FormBlockInitializer.tsx @@ -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 ( { componentType={'FormItem'} templateWrap={(templateSchema, { item }) => { const s = createFormBlockSchema({ + isCusomeizeCreate, template: templateSchema, collection: item.name, }); diff --git a/packages/core/client/src/schema-initializer/items/index.tsx b/packages/core/client/src/schema-initializer/items/index.tsx index 1f9a95bb0..fe0b4f558 100644 --- a/packages/core/client/src/schema-initializer/items/index.tsx +++ b/packages/core/client/src/schema-initializer/items/index.tsx @@ -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'; - diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index ae8c4edc4..7285930a8 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -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(); + 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 ( + + ); + }, + }, + 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 ( diff --git a/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx b/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx index 8960becd2..457cbee1a 100644 --- a/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx +++ b/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx @@ -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 ( - + ); diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx b/packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx new file mode 100644 index 000000000..ce1087529 --- /dev/null +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx @@ -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; + 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 => { + 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; +};