Merge pull request '子表单(弹窗)迁移到框架内' (#399) from subform_drawer_table into @hera/dev

Reviewed-on: daoyoucloud/tachycode#399
Reviewed-by: wangjiahui <wwwjh0710@163.com>
This commit is contained in:
sealday 2024-03-18 11:06:39 +08:00
commit 77585e6280
10 changed files with 24 additions and 353 deletions

View File

@ -267,6 +267,7 @@
"Sub-table": "Sub-table",
"Sub-details": "Sub-details",
"Sub-form(Popover)": "Sub-form(Popover)",
"Sub-form(Drawer)": "Sub-form(Drawer)",
"System info": "System info",
"Created at": "Created at",
"Last updated at": "Last updated at",

View File

@ -331,6 +331,7 @@
"Subtable": "子表格",
"Sub-form": "子表单",
"Sub-form(Popover)": "子表单(弹窗)",
"Sub-form(Drawer)": "子表单(抽屉)",
"Sub-details": "子详情",
"Record picker": "数据选择器",
"Toggles the subfield mode": "切换子字段模式",

View File

@ -4,12 +4,12 @@ import { RecursionField, connect, mapProps, observer, useField, useFieldSchema,
import { uid } from '@formily/shared';
import { Space, message } from 'antd';
import { isFunction } from 'mathjs';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RecordProvider, useAPIClient, useCollectionRecordData } from '../../../';
import { RecordProvider, useAPIClient, useApp, useCollectionRecordData } from '../../../';
import { isVariable } from '../../../variables/utils/isVariable';
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import { RemoteSelectProps } from '../remote-select';
import useServiceOptions, { useAssociationFieldContext } from './hooks';
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
@ -60,6 +60,9 @@ const InternalAssociationSelect = observer(
const resource = api.resource(collectionField.target);
const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter);
const recordData = useCollectionRecordData();
const app = useApp();
const RemoteSelect = useMemo(() => app.getComponent('RemoteSelect'), [app]);
useEffect(() => {
const initValue = isVariable(field.value) ? undefined : field.value;
const value = Array.isArray(initValue) ? initValue.filter(Boolean) : initValue;
@ -165,6 +168,8 @@ export const AssociationSelect = InternalAssociationSelect as unknown as Associa
export const AssociationSelectReadPretty = connect(
(props: any) => {
const service = useServiceOptions(props);
const app = useApp();
const RemoteSelect = useMemo(() => app.getComponent('RemoteSelect'), [app]) as any;
if (props.fieldNames) {
return <RemoteSelect.ReadPretty {...props} service={service}></RemoteSelect.ReadPretty>;
}

View File

@ -14,6 +14,8 @@ import { InternaPopoverNester } from './InternalPopoverNester';
import { InternalSubTable } from './InternalSubTable';
import { CreateRecordAction } from './components/CreateRecordAction';
import { useAssociationFieldContext } from './hooks';
import { InternalCascader } from './InternalCascader';
import { InternaDrawerSubTable } from './InternalDrawerSubTable';
const EditableAssociationField = observer(
(props: any) => {
@ -57,6 +59,8 @@ const EditableAssociationField = observer(
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
{currentMode === 'FileManager' && <InternalFileManager {...props} />}
{currentMode === 'CascadeSelect' && <InternalCascadeSelect {...props} />}
{currentMode === 'DrawerSubTable' && <InternaDrawerSubTable {...props} />}
{currentMode === 'Cascader' && <InternalCascader {...props} />}
</SchemaComponentOptions>
);
},

View File

@ -4,9 +4,12 @@ import { observer } from '@formily/react';
import React, { useContext, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InternalSubTable } from './InternalSubTable';
import { ActionContext, ActionContextProvider, __UNSAFE__, useGetAriaLabelOfPopover } from '@nocobase/client';
import { Button, Drawer } from 'antd';
const { useAssociationFieldContext, useSetAriaLabelForPopover, ReadPrettyInternalViewer } = __UNSAFE__;
import { useAssociationFieldContext } from './hooks';
import { ActionContext, ActionContextProvider } from '../action/context';
import { useGetAriaLabelOfPopover } from '../action';
import { ReadPrettyInternalViewer } from './InternalViewer';
import { useSetAriaLabelForPopover } from '../action/hooks/useSetAriaLabelForPopover';
export const InternaDrawerSubTable = observer(
(props) => {

View File

@ -55,6 +55,7 @@ export const useFieldModeOptions = (props?) => {
{ label: t('Cascade Select'), value: 'CascadeSelect' },
!isTableField && { label: t('Sub-form'), value: 'Nester' },
{ label: t('Sub-form(Popover)'), value: 'PopoverNester' },
{ label: t('Sub-form(Drawer)'), value: 'DrawerSubTable' },
{ label: t('Cascader'), value: 'Cascader' },
];
}
@ -72,6 +73,7 @@ export const useFieldModeOptions = (props?) => {
{ label: t('Record picker'), value: 'Picker' },
!isTableField && { label: t('Sub-form'), value: 'Nester' },
{ label: t('Sub-form(Popover)'), value: 'PopoverNester' },
{ label: t('Sub-form(Drawer)'), value: 'DrawerSubTable' },
!isTableField && { label: t('Sub-table'), value: 'SubTable' },
];
case 'm2m':
@ -88,6 +90,7 @@ export const useFieldModeOptions = (props?) => {
!isTableField && { label: t('Sub-table'), value: 'SubTable' },
!isTableField && { label: t('Sub-form'), value: 'Nester' },
{ label: t('Sub-form(Popover)'), value: 'PopoverNester' },
{ label: t('Sub-form(Drawer)'), value: 'DrawerSubTable' },
];
case 'm2o':
case 'linkTo':
@ -102,6 +105,7 @@ export const useFieldModeOptions = (props?) => {
{ label: t('Record picker'), value: 'Picker' },
!isTableField && { label: t('Sub-form'), value: 'Nester' },
{ label: t('Sub-form(Popover)'), value: 'PopoverNester' },
{ label: t('Sub-form(Drawer)'), value: 'DrawerSubTable' },
{ label: t('Cascader'), value: 'Cascader' },
];
@ -117,6 +121,7 @@ export const useFieldModeOptions = (props?) => {
{ label: t('Record picker'), value: 'Picker' },
!isTableField && { label: t('Sub-form'), value: 'Nester' },
{ label: t('Sub-form(Popover)'), value: 'PopoverNester' },
{ label: t('Sub-form(Drawer)'), value: 'DrawerSubTable' },
];
}
}, [t, collectionField?.interface, label]);

View File

@ -36,7 +36,6 @@ import { isValid } from '@formily/shared';
import { useCreateActionProps } from './hooks/useCreateActionProps';
import { PageLayout } from './pages/PageLayout';
import { useOutboundActionProps } from './hooks/useOutboundActionProps';
import { ExtendedAssociationField } from './schema-components/association-field/Editable';
import { AssociatedField } from './components/AssociatedField';
import { DatePicker } from './schema-components/date-picker';
import { Page } from './schema-components/page';
@ -162,7 +161,6 @@ export class PluginCoreClient extends Plugin {
DatePicker,
RemoteSelect,
SignatureInput,
AssociationField: ExtendedAssociationField,
OutboundButton,
OutboundLinkActionInitializer,
PDFViewerBlockInitializer,

View File

@ -1,182 +0,0 @@
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
import { onFieldChange, FormPath } from '@formily/core';
import { RecursionField, connect, mapProps, observer, useField, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { Space, message } from 'antd';
import { isFunction } from 'mathjs2';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RecordProvider, RemoteSelectProps, useAPIClient } from '@nocobase/client';
import { __UNSAFE__ } from '@nocobase/client';
import { RemoteSelect } from '../remote-select';
const { getInnermostKeyAndValue, useServiceOptions, useAssociationFieldContext, isVariable } = __UNSAFE__;
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
action?: string;
multiple?: boolean;
};
export const filterAnalyses = (filters): any[] => {
if (!filters) {
return;
}
const type = Object.keys(filters)[0] || '$and';
const conditions = filters[type];
const results = [];
conditions?.map((c) => {
const jsonlogic = getInnermostKeyAndValue(c);
const operator = jsonlogic?.key;
if (!operator) {
return true;
}
const regex = /\{\{\$(?:[a-zA-Z_]\w*)\.([a-zA-Z_]\w*)(?:\.id)?\}\}/;
const fieldName = jsonlogic?.value?.match?.(regex)?.[1];
if (fieldName) {
results.push(fieldName);
}
});
return results;
};
const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
const { objectValue = true } = props;
const field: any = useField();
const fieldSchema = useFieldSchema();
const service = useServiceOptions(props);
const { options: collectionField } = useAssociationFieldContext();
const initValue = isVariable(props.value) ? undefined : props.value;
const value = Array.isArray(initValue) ? initValue.filter(Boolean) : initValue;
// 因为通过 Schema 的形式书写的组件,在值变更的时候 `value` 的值没有改变,所以需要维护一个 `innerValue` 来变更值
const [innerValue, setInnerValue] = useState(value);
const addMode = fieldSchema['x-component-props']?.addMode;
const isAllowAddNew = fieldSchema['x-add-new'];
const { t } = useTranslation();
const { multiple } = props;
const form = useForm();
const api = useAPIClient();
const resource = api.resource(collectionField.target);
const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter);
useEffect(() => {
const initValue = isVariable(field.value) ? undefined : field.value;
const value = Array.isArray(initValue) ? initValue.filter(Boolean) : initValue;
setInnerValue(value);
}, [field.value]);
useEffect(() => {
const id = uid();
form.addEffects(id, () => {
if (linkageFields?.length > 0) {
//支持深层次子表单
onFieldChange('*', (fieldPath: any) => {
if (linkageFields.includes(fieldPath.props.name) && field.value) {
props.onChange(field.initialValue);
setInnerValue(field.initialValue);
}
});
}
});
return () => {
form.removeEffects(id);
};
}, []);
const handleCreateAction = async (props) => {
const { search: value, callBack } = props;
const {
data: { data },
} = await resource.create({
values: {
[field?.componentProps?.fieldNames?.label || 'id']: value,
},
});
if (data) {
if (['m2m', 'o2m'].includes(collectionField?.interface) && multiple !== false) {
const values = form.getValuesIn(field.path) || [];
values.push(data);
form.setValuesIn(field.path, values);
field.onInput(values);
} else {
form.setValuesIn(field.path, data);
field.onInput(data);
}
isFunction(callBack) && callBack?.();
message.success(t('Saved successfully'));
}
};
const QuickAddContent = (props) => {
return (
<div
onClick={() => handleCreateAction(props)}
style={{ cursor: 'pointer', padding: '5px 12px', color: '#0d0c0c' }}
>
<PlusOutlined />
<span style={{ paddingLeft: 5 }}>{t('Add') + `${props.search}`}</span>
</div>
);
};
return (
<div key={fieldSchema.name}>
<Space.Compact style={{ display: 'flex', lineHeight: '32px' }}>
<RemoteSelect
style={{ width: '100%' }}
{...props}
size={'middle'}
objectValue={objectValue}
value={value || innerValue}
service={service}
onChange={(value) => {
const val = value?.length !== 0 ? value : null;
props.onChange?.(val);
}}
CustomDropdownRender={addMode === 'quickAdd' && QuickAddContent}
></RemoteSelect>
{(addMode === 'modalAdd' || isAllowAddNew) && (
<RecordProvider record={null}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'Action';
}}
/>
</RecordProvider>
)}
</Space.Compact>
</div>
);
});
interface AssociationSelectInterface {
(props: any): React.ReactElement;
Designer: React.FC;
FilterDesigner: React.FC;
}
export const AssociationSelect = InternalAssociationSelect as unknown as AssociationSelectInterface;
export const AssociationSelectReadPretty = connect(
(props: any) => {
const service = useServiceOptions(props);
if (props.fieldNames) {
return <RemoteSelect.ReadPretty {...props} service={service}></RemoteSelect.ReadPretty>;
}
return null;
},
mapProps(
{
dataSource: 'options',
loading: true,
},
(props, field) => {
return {
...props,
fieldNames: props.fieldNames && { ...props.fieldNames, ...field.componentProps.fieldNames },
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon,
};
},
),
);

View File

@ -1,95 +0,0 @@
import { Field } from '@formily/core';
import { connect, mapReadPretty, observer, useField, useForm } from '@formily/react';
import React from 'react';
import {
SchemaComponentOptions,
useCollection_deprecated,
useAssociationCreateActionProps as useCAP,
Action,
__UNSAFE__,
} from '@nocobase/client';
import { InternaDrawerSubTable } from './InternalDrawerSubTable';
import { AssociationSelect } from './AssociationSelect';
const {
CreateRecordAction,
useAssociationFieldContext,
InternalPicker,
InternalNester,
AssociationFieldProvider,
InternaPopoverNester,
InternalSubTable,
InternalCascader,
InternalFileManager,
InternalCascadeSelect,
SubTable,
ReadPretty,
Nester,
} = __UNSAFE__;
const EditableAssociationField = observer(
(props: any) => {
const { multiple } = props;
const field: Field = useField();
const form = useForm();
const { options: collectionField, currentMode } = useAssociationFieldContext();
const useCreateActionProps = () => {
const { onClick } = useCAP();
const actionField: any = useField();
const { getPrimaryKey } = useCollection_deprecated();
const primaryKey = getPrimaryKey();
return {
async onClick() {
await onClick();
const { data } = actionField.data?.data?.data || {};
if (data) {
if (['m2m', 'o2m'].includes(collectionField?.interface) && multiple !== false) {
const values = form.getValuesIn(field.path) || [];
if (!values.find((v) => v[primaryKey] === data[primaryKey])) {
values.push(data);
form.setValuesIn(field.path, values);
field.onInput(values);
}
} else {
form.setValuesIn(field.path, data);
field.onInput(data);
}
}
},
};
};
return (
<SchemaComponentOptions scope={{ useCreateActionProps }} components={{ CreateRecordAction }}>
{currentMode === 'Picker' && <InternalPicker {...props} />}
{currentMode === 'Nester' && <InternalNester {...props} />}
{currentMode === 'PopoverNester' && <InternaPopoverNester {...props} />}
{currentMode === 'Select' && <AssociationSelect {...props} />}
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
{currentMode === 'FileManager' && <InternalFileManager {...props} />}
{currentMode === 'CascadeSelect' && <InternalCascadeSelect {...props} />}
{currentMode === 'DrawerSubTable' && <InternaDrawerSubTable {...props} />}
{currentMode === 'Cascader' && <InternalCascader {...props} />}
</SchemaComponentOptions>
);
},
{ displayName: 'EditableAssociationField' },
);
const ExtendedEditable = observer(
(props) => {
return (
<AssociationFieldProvider>
<EditableAssociationField {...props} />
</AssociationFieldProvider>
);
},
{ displayName: 'ExtendedEditable' },
);
export const ExtendedAssociationField: any = connect(ExtendedEditable, mapReadPretty(ReadPretty));
ExtendedAssociationField.SubTable = SubTable;
ExtendedAssociationField.Nester = Nester;
ExtendedAssociationField.AddNewer = Action.Container;
ExtendedAssociationField.Selector = Action.Container;
ExtendedAssociationField.Viewer = Action.Container;
ExtendedAssociationField.InternalSelect = InternalPicker;

View File

@ -1,69 +0,0 @@
import { css, cx } from '@emotion/css';
import { FormLayout } from '@formily/antd-v5';
import { RecursionField, useField, useFieldSchema, observer } from '@formily/react';
import React, { useEffect } from 'react';
import { useInsertSchema } from '../../components/AssociatedField';
import {
ACLCollectionProvider,
CollectionProvider_deprecated,
__UNSAFE__,
useACLActionParamsContext,
} from '@nocobase/client';
const { useAssociationFieldContext, schema } = __UNSAFE__;
export const InternalSubTable = observer(
() => {
const field = useField();
const fieldSchema = useFieldSchema();
const insertNester = useInsertSchema('SubTable');
const { options: collectionField } = useAssociationFieldContext();
const showTitle = fieldSchema['x-decorator-props']?.showTitle ?? true;
const { actionName } = useACLActionParamsContext();
useEffect(() => {
insertNester(schema.SubTable);
}, []);
return (
<CollectionProvider_deprecated name={collectionField.target}>
<ACLCollectionProvider actionPath={`${collectionField.target}:${actionName}`}>
<FormLayout layout={'vertical'}>
<div
className={cx(
css`
& .ant-formily-item-layout-vertical {
margin-bottom: 10px;
}
.ant-card-body {
padding: 15px 20px 5px;
}
.ant-divider-horizontal {
margin: 10px 0;
}
`,
{
[css`
.ant-card-body {
padding: 0px 20px 20px 0px;
}
> .ant-card-bordered {
border: none;
}
`]: showTitle === false,
},
)}
>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.SubTable';
}}
/>
</div>
</FormLayout>
</ACLCollectionProvider>
</CollectionProvider_deprecated>
);
},
{ displayName: 'InternalSubTable' },
);