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:
commit
77585e6280
@ -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",
|
||||
|
@ -331,6 +331,7 @@
|
||||
"Subtable": "子表格",
|
||||
"Sub-form": "子表单",
|
||||
"Sub-form(Popover)": "子表单(弹窗)",
|
||||
"Sub-form(Drawer)": "子表单(抽屉)",
|
||||
"Sub-details": "子详情",
|
||||
"Record picker": "数据选择器",
|
||||
"Toggles the subfield mode": "切换子字段模式",
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
@ -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) => {
|
@ -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]);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
@ -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;
|
@ -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' },
|
||||
);
|
Loading…
Reference in New Issue
Block a user