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",
|
"version": "1.6.0-alpha.5",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"npmClientArgs": [
|
"npmClientArgs": ["--ignore-engines"],
|
||||||
"--ignore-engines"
|
|
||||||
],
|
|
||||||
"command": {
|
"command": {
|
||||||
"version": {
|
"version": {
|
||||||
"forcePublish": true,
|
"forcePublish": true,
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
} from '../../../../schema-component/antd/form-item/FormItem.Settings';
|
} from '../../../../schema-component/antd/form-item/FormItem.Settings';
|
||||||
import { linkageRules, setDefaultSortingRules } from '../SubTable/subTablePopoverComponentFieldSettings';
|
import { linkageRules, setDefaultSortingRules } from '../SubTable/subTablePopoverComponentFieldSettings';
|
||||||
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
|
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
|
||||||
|
import { allowSelectExistingRecord } from '../SubTable/subTablePopoverComponentFieldSettings';
|
||||||
|
|
||||||
const allowMultiple: any = {
|
const allowMultiple: any = {
|
||||||
name: 'allowMultiple',
|
name: 'allowMultiple',
|
||||||
@ -108,6 +109,7 @@ export const subformComponentFieldSettings = new SchemaSettings({
|
|||||||
items: [
|
items: [
|
||||||
fieldComponent,
|
fieldComponent,
|
||||||
allowMultiple,
|
allowMultiple,
|
||||||
|
allowSelectExistingRecord,
|
||||||
{
|
{
|
||||||
name: 'allowDissociate',
|
name: 'allowDissociate',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
|
@ -72,7 +72,7 @@ const fieldComponent: any = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const allowSelectExistingRecord = {
|
export const allowSelectExistingRecord = {
|
||||||
name: 'allowSelectExistingRecord',
|
name: 'allowSelectExistingRecord',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
useVisible() {
|
useVisible() {
|
||||||
|
@ -44,6 +44,7 @@ export const InternalNester = observer(
|
|||||||
const field = useField();
|
const field = useField();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const insertNester = useInsertSchema('Nester');
|
const insertNester = useInsertSchema('Nester');
|
||||||
|
const insertSelector = useInsertSchema('Selector');
|
||||||
const { options: collectionField } = useAssociationFieldContext();
|
const { options: collectionField } = useAssociationFieldContext();
|
||||||
const showTitle = fieldSchema['x-decorator-props']?.showTitle ?? true;
|
const showTitle = fieldSchema['x-decorator-props']?.showTitle ?? true;
|
||||||
const { actionName } = useACLActionParamsContext();
|
const { actionName } = useACLActionParamsContext();
|
||||||
@ -58,7 +59,11 @@ export const InternalNester = observer(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
insertNester(schema.Nester);
|
insertNester(schema.Nester);
|
||||||
}, []);
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (field.componentProps?.allowSelectExistingRecord) {
|
||||||
|
insertSelector(schema.Selector);
|
||||||
|
}
|
||||||
|
}, [field.componentProps?.allowSelectExistingRecord]);
|
||||||
return (
|
return (
|
||||||
<CollectionProvider_deprecated name={collectionField.target}>
|
<CollectionProvider_deprecated name={collectionField.target}>
|
||||||
<ACLCollectionProvider actionPath={`${collectionField.target}:${actionName || 'view'}`}>
|
<ACLCollectionProvider actionPath={`${collectionField.target}:${actionName || 'view'}`}>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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 { css } from '@emotion/css';
|
||||||
import { ArrayField } from '@formily/core';
|
import { ArrayField } from '@formily/core';
|
||||||
import { spliceArrayState } from '@formily/core/esm/shared/internals';
|
import { spliceArrayState } from '@formily/core/esm/shared/internals';
|
||||||
@ -15,11 +15,12 @@ import { observer, useFieldSchema } from '@formily/react';
|
|||||||
import { action } from '@formily/reactive';
|
import { action } from '@formily/reactive';
|
||||||
import { each } from '@formily/shared';
|
import { each } from '@formily/shared';
|
||||||
import { useUpdate } from 'ahooks';
|
import { useUpdate } from 'ahooks';
|
||||||
import { Button, Card, Divider, Tooltip } from 'antd';
|
import { Button, Card, Divider, Tooltip, Space } from 'antd';
|
||||||
import React, { useCallback, useContext } from 'react';
|
import React, { useCallback, useContext, useState, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FormActiveFieldsProvider } from '../../../block-provider/hooks/useFormActiveFields';
|
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 {
|
import {
|
||||||
useCollectionRecord,
|
useCollectionRecord,
|
||||||
useCollectionRecordData,
|
useCollectionRecordData,
|
||||||
@ -29,13 +30,25 @@ import { FlagProvider } from '../../../flag-provider';
|
|||||||
import { NocoBaseRecursionField, RefreshComponentProvider } from '../../../formily/NocoBaseRecursionField';
|
import { NocoBaseRecursionField, RefreshComponentProvider } from '../../../formily/NocoBaseRecursionField';
|
||||||
import { RecordIndexProvider, RecordProvider } from '../../../record-provider';
|
import { RecordIndexProvider, RecordProvider } from '../../../record-provider';
|
||||||
import { isPatternDisabled, isSystemField } from '../../../schema-settings';
|
import { isPatternDisabled, isSystemField } from '../../../schema-settings';
|
||||||
|
import { TableSelectorParamsProvider } from '../../../block-provider/TableSelectorProvider';
|
||||||
import {
|
import {
|
||||||
DefaultValueProvider,
|
DefaultValueProvider,
|
||||||
IsAllowToSetDefaultValueParams,
|
IsAllowToSetDefaultValueParams,
|
||||||
interfacesOfUnsupportedDefaultValue,
|
interfacesOfUnsupportedDefaultValue,
|
||||||
} from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
} from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
||||||
import { AssociationFieldContext } from './context';
|
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) => {
|
export const Nester = (props) => {
|
||||||
const { options } = useContext(AssociationFieldContext);
|
const { options } = useContext(AssociationFieldContext);
|
||||||
@ -109,14 +122,32 @@ const ToOneNester = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ToManyNester = observer(
|
const ToManyNester = observer(
|
||||||
(props) => {
|
(props: any) => {
|
||||||
const fieldSchema = useFieldSchema();
|
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 { t } = useTranslation();
|
||||||
const recordData = useCollectionRecordData();
|
const recordData = useCollectionRecordData();
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
const update = useUpdate();
|
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)) {
|
if (!Array.isArray(field.value)) {
|
||||||
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 ? (
|
return field.value.length > 0 ? (
|
||||||
<Card
|
<Card
|
||||||
bordered={true}
|
bordered={true}
|
||||||
@ -160,32 +239,11 @@ const ToManyNester = observer(
|
|||||||
{field.value.map((value, index) => {
|
{field.value.map((value, index) => {
|
||||||
let allowed = allowDissociate;
|
let allowed = allowDissociate;
|
||||||
if (!allowDissociate) {
|
if (!allowDissociate) {
|
||||||
allowed = !value?.[options.targetKey];
|
allowed = !value?.[collectionField.targetKey];
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<div style={{ textAlign: 'right' }}>
|
<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 && (
|
{!field.readPretty && allowed && (
|
||||||
<Tooltip key={'remove'} title={t('Remove')}>
|
<Tooltip key={'remove'} title={t('Remove')}>
|
||||||
<CloseOutlined
|
<CloseOutlined
|
||||||
@ -219,11 +277,85 @@ const ToManyNester = observer(
|
|||||||
</RecordProvider>
|
</RecordProvider>
|
||||||
</SubFormProvider>
|
</SubFormProvider>
|
||||||
</FormActiveFieldsProvider>
|
</FormActiveFieldsProvider>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
</React.Fragment>
|
</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>
|
</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>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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 { css } from '@emotion/css';
|
||||||
import { ArrayField } from '@formily/core';
|
import { ArrayField } from '@formily/core';
|
||||||
import { exchangeArrayState } from '@formily/core/esm/shared/internals';
|
import { exchangeArrayState } from '@formily/core/esm/shared/internals';
|
||||||
@ -259,7 +259,7 @@ export const SubTable: any = observer(
|
|||||||
useAction={useSubTableAddNewProps}
|
useAction={useSubTableAddNewProps}
|
||||||
title={
|
title={
|
||||||
<Space style={{ gap: 2 }} className="nb-sub-table-addNew">
|
<Space style={{ gap: 2 }} className="nb-sub-table-addNew">
|
||||||
<PlusSquareOutlined /> {t('Add new')}
|
<PlusOutlined /> {t('Add new')}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -54,14 +54,10 @@ test.describe('form item & sub form', () => {
|
|||||||
unsupportedVariables: ['Parent popup record'],
|
unsupportedVariables: ['Parent popup record'],
|
||||||
variableValue: ['Current user', 'Nickname'],
|
variableValue: ['Current user', 'Nickname'],
|
||||||
expectVariableValue: async () => {
|
expectVariableValue: async () => {
|
||||||
await page
|
await page.locator('.nb-sub-form-addNew').first().click();
|
||||||
.getByLabel('block-item-CollectionField-subform-form-subform.manyToMany-manyToMany')
|
|
||||||
.getByRole('img', { name: 'plus' })
|
|
||||||
.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',
|
'Super Admin',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user