mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
feat: support selecting existing data in subform (#5849)
* feat: subform support selecting existing data * chore: add new button in sub form * fix: build * fix: bug * fix: test * fix: test
This commit is contained in:
parent
5132d460c1
commit
e04c404b4c
@ -2,9 +2,7 @@
|
||||
"version": "1.6.0-alpha.5",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": [
|
||||
"--ignore-engines"
|
||||
],
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
"command": {
|
||||
"version": {
|
||||
"forcePublish": true,
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
} from '../../../../schema-component/antd/form-item/FormItem.Settings';
|
||||
import { linkageRules, setDefaultSortingRules } from '../SubTable/subTablePopoverComponentFieldSettings';
|
||||
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
|
||||
import { allowSelectExistingRecord } from '../SubTable/subTablePopoverComponentFieldSettings';
|
||||
|
||||
const allowMultiple: any = {
|
||||
name: 'allowMultiple',
|
||||
@ -108,6 +109,7 @@ export const subformComponentFieldSettings = new SchemaSettings({
|
||||
items: [
|
||||
fieldComponent,
|
||||
allowMultiple,
|
||||
allowSelectExistingRecord,
|
||||
{
|
||||
name: 'allowDissociate',
|
||||
type: 'switch',
|
||||
|
@ -72,7 +72,7 @@ const fieldComponent: any = {
|
||||
};
|
||||
},
|
||||
};
|
||||
const allowSelectExistingRecord = {
|
||||
export const allowSelectExistingRecord = {
|
||||
name: 'allowSelectExistingRecord',
|
||||
type: 'switch',
|
||||
useVisible() {
|
||||
|
@ -44,6 +44,7 @@ export const InternalNester = observer(
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const insertNester = useInsertSchema('Nester');
|
||||
const insertSelector = useInsertSchema('Selector');
|
||||
const { options: collectionField } = useAssociationFieldContext();
|
||||
const showTitle = fieldSchema['x-decorator-props']?.showTitle ?? true;
|
||||
const { actionName } = useACLActionParamsContext();
|
||||
@ -58,7 +59,11 @@ export const InternalNester = observer(
|
||||
useEffect(() => {
|
||||
insertNester(schema.Nester);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (field.componentProps?.allowSelectExistingRecord) {
|
||||
insertSelector(schema.Selector);
|
||||
}
|
||||
}, [field.componentProps?.allowSelectExistingRecord]);
|
||||
return (
|
||||
<CollectionProvider_deprecated name={collectionField.target}>
|
||||
<ACLCollectionProvider actionPath={`${collectionField.target}:${actionName || 'view'}`}>
|
||||
|
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { CloseOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { CloseOutlined, PlusOutlined, ZoomInOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { spliceArrayState } from '@formily/core/esm/shared/internals';
|
||||
@ -15,11 +15,12 @@ import { observer, useFieldSchema } from '@formily/react';
|
||||
import { action } from '@formily/reactive';
|
||||
import { each } from '@formily/shared';
|
||||
import { useUpdate } from 'ahooks';
|
||||
import { Button, Card, Divider, Tooltip } from 'antd';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { Button, Card, Divider, Tooltip, Space } from 'antd';
|
||||
import React, { useCallback, useContext, useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormActiveFieldsProvider } from '../../../block-provider/hooks/useFormActiveFields';
|
||||
import { useCollection } from '../../../data-source';
|
||||
import { useCollection, CollectionProvider } from '../../../data-source';
|
||||
import { useCreateActionProps } from '../../../block-provider/hooks';
|
||||
import {
|
||||
useCollectionRecord,
|
||||
useCollectionRecordData,
|
||||
@ -29,13 +30,25 @@ import { FlagProvider } from '../../../flag-provider';
|
||||
import { NocoBaseRecursionField, RefreshComponentProvider } from '../../../formily/NocoBaseRecursionField';
|
||||
import { RecordIndexProvider, RecordProvider } from '../../../record-provider';
|
||||
import { isPatternDisabled, isSystemField } from '../../../schema-settings';
|
||||
import { TableSelectorParamsProvider } from '../../../block-provider/TableSelectorProvider';
|
||||
import {
|
||||
DefaultValueProvider,
|
||||
IsAllowToSetDefaultValueParams,
|
||||
interfacesOfUnsupportedDefaultValue,
|
||||
} from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
||||
import { AssociationFieldContext } from './context';
|
||||
import { SubFormProvider, useAssociationFieldContext } from './hooks';
|
||||
import { SubFormProvider, useAssociationFieldContext, useFieldNames } from './hooks';
|
||||
import { Action, ActionContextProvider } from '../action';
|
||||
import { useCompile } from '../../hooks';
|
||||
import {
|
||||
FormProvider,
|
||||
RecordPickerProvider,
|
||||
SchemaComponentOptions,
|
||||
useActionContext,
|
||||
RecordPickerContext,
|
||||
} from '../..';
|
||||
import { useTableSelectorProps } from './InternalPicker';
|
||||
import { getLabelFormatValue, useLabelUiSchema } from './util';
|
||||
|
||||
export const Nester = (props) => {
|
||||
const { options } = useContext(AssociationFieldContext);
|
||||
@ -109,14 +122,32 @@ const ToOneNester = (props) => {
|
||||
};
|
||||
|
||||
const ToManyNester = observer(
|
||||
(props) => {
|
||||
(props: any) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { options, field, allowMultiple, allowDissociate } = useAssociationFieldContext<ArrayField>();
|
||||
const {
|
||||
options: collectionField,
|
||||
field,
|
||||
allowMultiple,
|
||||
allowDissociate,
|
||||
currentMode,
|
||||
} = useAssociationFieldContext<ArrayField>();
|
||||
const { allowSelectExistingRecord } = field.componentProps;
|
||||
const { t } = useTranslation();
|
||||
const recordData = useCollectionRecordData();
|
||||
const collection = useCollection();
|
||||
const update = useUpdate();
|
||||
|
||||
const [visibleSelector, setVisibleSelector] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
const fieldNames = useFieldNames(props);
|
||||
const compile = useCompile();
|
||||
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
|
||||
const useNesterSelectProps = () => {
|
||||
return {
|
||||
run() {
|
||||
setVisibleSelector(true);
|
||||
},
|
||||
};
|
||||
};
|
||||
if (!Array.isArray(field.value)) {
|
||||
field.value = [];
|
||||
}
|
||||
@ -146,6 +177,54 @@ const ToManyNester = observer(
|
||||
);
|
||||
}, []);
|
||||
|
||||
const usePickActionProps = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
const { selectedRows, setSelectedRows } = useContext(RecordPickerContext);
|
||||
return {
|
||||
onClick() {
|
||||
selectedRows.map((v) => field.value.push(markRecordAsNew(v)));
|
||||
field.onInput(field.value);
|
||||
field.initialValue = field.value;
|
||||
setSelectedRows([]);
|
||||
setVisible(false);
|
||||
},
|
||||
};
|
||||
};
|
||||
const options = useMemo(() => {
|
||||
if (field.value && Object.keys(field.value).length > 0) {
|
||||
const opts = (Array.isArray(field.value) ? field.value : field.value ? [field.value] : [])
|
||||
.filter(Boolean)
|
||||
.map((option) => {
|
||||
const label = option?.[fieldNames.label];
|
||||
return {
|
||||
...option,
|
||||
[fieldNames.label]: getLabelFormatValue(compile(labelUiSchema), compile(label)),
|
||||
};
|
||||
});
|
||||
return opts;
|
||||
}
|
||||
return [];
|
||||
}, [field.value, fieldNames?.label]);
|
||||
|
||||
const pickerProps = {
|
||||
size: 'small',
|
||||
fieldNames: field.componentProps.fieldNames,
|
||||
multiple: true,
|
||||
association: {
|
||||
target: collectionField?.target,
|
||||
},
|
||||
options,
|
||||
onChange: props?.onChange,
|
||||
selectedRows,
|
||||
setSelectedRows,
|
||||
collectionField,
|
||||
};
|
||||
const getFilter = () => {
|
||||
const targetKey = collectionField?.targetKey || 'id';
|
||||
const list = (field.value || []).map((option) => option?.[targetKey]).filter(Boolean);
|
||||
const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {};
|
||||
return filter;
|
||||
};
|
||||
return field.value.length > 0 ? (
|
||||
<Card
|
||||
bordered={true}
|
||||
@ -160,32 +239,11 @@ const ToManyNester = observer(
|
||||
{field.value.map((value, index) => {
|
||||
let allowed = allowDissociate;
|
||||
if (!allowDissociate) {
|
||||
allowed = !value?.[options.targetKey];
|
||||
allowed = !value?.[collectionField.targetKey];
|
||||
}
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
{field.editable && allowMultiple && (
|
||||
<Tooltip key={'add'} title={t('Add new')}>
|
||||
<PlusOutlined
|
||||
style={{ zIndex: 1000, marginRight: '10px', color: '#a8a3a3' }}
|
||||
onClick={() => {
|
||||
action(() => {
|
||||
if (!Array.isArray(field.value)) {
|
||||
field.value = [];
|
||||
}
|
||||
field.value.splice(index + 1, 0, markRecordAsNew({}));
|
||||
each(field.form.fields, (targetField, key) => {
|
||||
if (!targetField) {
|
||||
delete field.form.fields[key];
|
||||
}
|
||||
});
|
||||
return field.onInput(field.value);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!field.readPretty && allowed && (
|
||||
<Tooltip key={'remove'} title={t('Remove')}>
|
||||
<CloseOutlined
|
||||
@ -219,11 +277,85 @@ const ToManyNester = observer(
|
||||
</RecordProvider>
|
||||
</SubFormProvider>
|
||||
</FormActiveFieldsProvider>
|
||||
|
||||
<Divider />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<Space>
|
||||
{field.editable && allowMultiple && (
|
||||
<Action.Link
|
||||
useProps={() => {
|
||||
return {
|
||||
onClick: () => {
|
||||
action(() => {
|
||||
if (!Array.isArray(field.value)) {
|
||||
field.value = [];
|
||||
}
|
||||
const index = field.value.length;
|
||||
field.value.splice(index, 0, markRecordAsNew({}));
|
||||
each(field.form.fields, (targetField, key) => {
|
||||
if (!targetField) {
|
||||
delete field.form.fields[key];
|
||||
}
|
||||
});
|
||||
return field.onInput(field.value);
|
||||
});
|
||||
},
|
||||
};
|
||||
}}
|
||||
title={
|
||||
<Space style={{ gap: 2 }} className="nb-sub-form-addNew">
|
||||
<PlusOutlined /> {t('Add new')}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{field.editable && allowSelectExistingRecord && currentMode === 'Nester' && (
|
||||
<Action.Link
|
||||
useAction={useNesterSelectProps}
|
||||
title={
|
||||
<Space style={{ gap: 2 }}>
|
||||
<ZoomInOutlined /> {t('Select record')}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</RefreshComponentProvider>
|
||||
<ActionContextProvider
|
||||
value={{
|
||||
openSize: 'middle',
|
||||
openMode: 'drawer',
|
||||
visible: visibleSelector,
|
||||
setVisible: setVisibleSelector,
|
||||
}}
|
||||
>
|
||||
<RecordPickerProvider {...pickerProps}>
|
||||
<CollectionProvider name={collectionField?.target}>
|
||||
<FormProvider>
|
||||
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
|
||||
<SchemaComponentOptions
|
||||
scope={{
|
||||
usePickActionProps,
|
||||
useTableSelectorProps,
|
||||
useCreateActionProps,
|
||||
}}
|
||||
>
|
||||
<NocoBaseRecursionField
|
||||
onlyRenderProperties
|
||||
basePath={field.address}
|
||||
schema={fieldSchema.parent}
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === 'AssociationField.Selector';
|
||||
}}
|
||||
/>
|
||||
</SchemaComponentOptions>
|
||||
</TableSelectorParamsProvider>
|
||||
</FormProvider>
|
||||
</CollectionProvider>
|
||||
</RecordPickerProvider>
|
||||
</ActionContextProvider>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
|
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { PlusSquareOutlined, ZoomInOutlined } from '@ant-design/icons';
|
||||
import { PlusOutlined, ZoomInOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { exchangeArrayState } from '@formily/core/esm/shared/internals';
|
||||
@ -259,7 +259,7 @@ export const SubTable: any = observer(
|
||||
useAction={useSubTableAddNewProps}
|
||||
title={
|
||||
<Space style={{ gap: 2 }} className="nb-sub-table-addNew">
|
||||
<PlusSquareOutlined /> {t('Add new')}
|
||||
<PlusOutlined /> {t('Add new')}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
@ -54,14 +54,10 @@ test.describe('form item & sub form', () => {
|
||||
unsupportedVariables: ['Parent popup record'],
|
||||
variableValue: ['Current user', 'Nickname'],
|
||||
expectVariableValue: async () => {
|
||||
await page
|
||||
.getByLabel('block-item-CollectionField-subform-form-subform.manyToMany-manyToMany')
|
||||
.getByRole('img', { name: 'plus' })
|
||||
.first()
|
||||
.click();
|
||||
await page.locator('.nb-sub-form-addNew').first().click();
|
||||
|
||||
// 在第一条数据下面增加一条数据
|
||||
await expect(page.getByLabel('block-item-CollectionField-').nth(2).getByRole('textbox')).toHaveValue(
|
||||
// 在最下面增加一条数据
|
||||
await expect(page.getByLabel('block-item-CollectionField-').last().getByRole('textbox')).toHaveValue(
|
||||
'Super Admin',
|
||||
);
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user