refactor(sub-table): sub-table support selection of existing records (#3311)

* refactor: sub-table support selection of existing records

* refactor: local improve

* refactor: sub-table support select existing records

* refactor: create action support updateAssociationValues

* refactor:  sub-table

* fix: omit foreignKey

* refactor: record picker omit foreignKey

* test: manyToMany

* test: subform: basic fields

* test: table column & sub-table in edit form
This commit is contained in:
katherinehhh 2024-01-15 17:46:55 +08:00 committed by GitHub
parent dc71a77d8c
commit 6fb5af993e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 253 additions and 34 deletions

View File

@ -268,10 +268,17 @@ export const useTableSelectorContext = () => {
export const useTableSelectorProps = () => {
const field = useField<ArrayField>();
const ctx = useTableSelectorContext();
const fieldSchema = useFieldSchema();
const { getCollectionJoinField } = useCollectionManager();
const collectionFieldSchema = recursiveParent(fieldSchema, 'CollectionField');
const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']);
useEffect(() => {
if (!ctx?.service?.loading) {
field.value = ctx?.service?.data?.data;
field?.setInitialValue?.(ctx?.service?.data?.data);
const data = ctx?.service?.data?.data.map((v) => {
return _.omit(v, collectionField?.foreignKey);
});
field.value = data;
field?.setInitialValue?.(data);
field.data = field.data || {};
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
field.componentProps.pagination = field.componentProps.pagination || {};

View File

@ -184,6 +184,7 @@ export const useCreateActionProps = () => {
const compile = useCompile();
const { modal } = App.useApp();
const { t } = useTranslation();
const { updateAssociationValues } = useFormBlockContext();
const collectValues = useCollectValuesToSubmit();
const action = actionField.componentProps.saveMode || 'create';
const filterKeys = actionField.componentProps.filterKeys?.checked || [];
@ -205,6 +206,7 @@ export const useCreateActionProps = () => {
triggerWorkflows: triggerWorkflows?.length
? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',')
: undefined,
updateAssociationValues,
});
setVisible?.(false);
actionField.data.loading = false;

View File

@ -749,5 +749,7 @@
"Current form": "Formulaire actuel",
"Current object": "Objet actuel",
"Linkage with form fields": "Lien avec les champs de formulaire",
"Allow add new, update and delete actions": "Autoriser les actions d'ajout, de mise à jour et de suppression"
"Allow add new, update and delete actions": "Autoriser les actions d'ajout, de mise à jour et de suppression",
"Allow add new":"Autoriser les ajouts",
"Allow selection of existing records":"Permet de sélectionner des données existantes"
}

View File

@ -666,5 +666,7 @@
"Search plugin": "プラグインを検索",
"Author": "著者",
"Plugin loading failed. Please check the server logs.": "プラグインのロードに失敗しました。サーバーログを確認してください。",
"Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "テーブルに依存するオブジェクト、およびそれらに依存するオブジェクトを自動的に削除する"
"Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "テーブルに依存するオブジェクト、およびそれらに依存するオブジェクトを自動的に削除する",
"Allow add new":"新規作成を許可",
"Allow selection of existing records":"既存のデータの選択を許可"
}

View File

@ -708,5 +708,7 @@
"Data template": "Modelo de dados",
"Not found": "Não encontrado",
"Add": "Adicionar",
"Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "Excluir automaticamente objetos que dependem desta tabela, bem como objetos que dependem desses objetos"
"Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "Excluir automaticamente objetos que dependem desta tabela, bem como objetos que dependem desses objetos",
"Allow add new":"Permitir novas adições",
"Allow selection of existing records":"Permitir a selecção dos registos existentes"
}

View File

@ -839,5 +839,7 @@
"Are you sure you want to perform the {{title}} action?": "你确定执行{{title}}操作吗?",
"Sign in with another account": "登录其他账号",
"Return to the main application": "返回主应用",
"Permission denied": "没有权限"
"Permission denied": "没有权限",
"Allow add new":"允许新增",
"Allow selection of existing records":"允许选择已有数据"
}

View File

@ -597,7 +597,10 @@ test.describe('creation form block schema settings', () => {
await page.mouse.move(300, 0);
// 当新增一行时,应该显示默认值
await page.getByRole('button', { name: 'plus' }).click();
await page
.getByTestId('drawer-Action.Container-general-Add record')
.getByRole('button', { name: 'Add new' })
.click();
await expect(
page
.getByRole('cell', { name: 'block-item-CollectionField-users-form-users.nickname-Nickname' })

View File

@ -21,7 +21,7 @@ import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './ho
import schema from './schema';
import { flatData, getLabelFormatValue, useLabelUiSchema } from './util';
const useTableSelectorProps = () => {
export const useTableSelectorProps = () => {
const field: any = useField();
const {
multiple,

View File

@ -14,13 +14,18 @@ export const InternalSubTable = observer(
const field: any = useField();
const fieldSchema = useFieldSchema();
const insert = useInsertSchema('SubTable');
const insertSelector = useInsertSchema('Selector');
const { options } = useAssociationFieldContext();
const { actionName } = useACLActionParamsContext();
useEffect(() => {
insert(schema.SubTable);
field.required = fieldSchema['required'];
}, []);
useEffect(() => {
if (field.componentProps?.allowSelectExistingRecord) {
insertSelector(schema.Selector);
}
}, [field.componentProps?.allowSelectExistingRecord]);
const option = useSchemaOptionsContext();
const components = {
...option.components,

View File

@ -1,21 +1,44 @@
import { PlusOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { ArrayField } from '@formily/core';
import { exchangeArrayState } from '@formily/core/esm/shared/internals';
import { observer } from '@formily/react';
import { observer, RecursionField, useFieldSchema } from '@formily/react';
import { action } from '@formily/reactive';
import { isArr } from '@formily/shared';
import { Button } from 'antd';
import React from 'react';
import { omit, unionBy, uniqBy } from 'lodash';
import React, { useState, useMemo, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { FormActiveFieldsProvider } from '../../../block-provider';
import { FlagProvider } from '../../../flag-provider';
import { Table } from '../table-v2/Table';
import { useAssociationFieldContext } from './hooks';
import { useAssociationFieldContext, useFieldNames } from './hooks';
import { ActionContextProvider } from '../action';
import {
FormProvider,
RecordPickerProvider,
SchemaComponentOptions,
useActionContext,
RecordPickerContext,
} from '../..';
import { getLabelFormatValue, useLabelUiSchema } from './util';
import { CollectionProvider } from '../../../collection-manager';
import { TableSelectorParamsProvider } from '../../../block-provider/TableSelectorProvider';
import { useCompile } from '../../hooks';
import { useTableSelectorProps } from './InternalPicker';
import { useCreateActionProps } from '../../../block-provider/hooks';
export const SubTable: any = observer(
(props: any) => {
const { field } = useAssociationFieldContext<ArrayField>();
// useSubTableSpecialCase({ field });
const { openSize } = props;
const { field, options: collectionField } = useAssociationFieldContext<ArrayField>();
const { t } = useTranslation();
const [visibleSelector, setVisibleSelector] = useState(false);
const [selectedRows, setSelectedRows] = useState([]);
const fieldNames = useFieldNames(props);
const fieldSchema = useFieldSchema();
const compile = useCompile();
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const move = (fromIndex: number, toIndex: number) => {
if (toIndex === undefined) return;
if (!isArr(field.value)) return;
@ -32,6 +55,55 @@ export const SubTable: any = observer(
});
};
field.move = move;
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 usePickActionProps = () => {
const { setVisible } = useActionContext();
const { selectedRows, options, collectionField } = useContext(RecordPickerContext);
return {
onClick() {
const selectData = unionBy(selectedRows, options, collectionField?.targetKey || 'id');
const data = field.value || [];
field.value = uniqBy(data.concat(selectData), collectionField?.targetKey || 'id');
field.onInput(field.value);
setVisible(false);
},
};
};
const getFilter = () => {
const targetKey = collectionField?.targetKey || 'id';
const list = options.map((option) => option[targetKey]).filter(Boolean);
const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {};
return filter;
};
return (
<div
className={css`
@ -72,6 +144,9 @@ export const SubTable: any = observer(
.ant-formily-editable {
vertical-align: sub;
}
.ant-table-footer {
display: flex;
}
`}
bordered
size={'small'}
@ -83,27 +158,80 @@ export const SubTable: any = observer(
rowSelection={{ type: 'none', hideSelectAll: true }}
footer={() =>
field.editable && (
<>
{field.componentProps?.allowAddnew !== false && (
<Button
type={'text'}
block
className={css`
display: block;
border-radius: 0px;
border-right: 1px solid rgba(0, 0, 0, 0.06);
`}
onClick={() => {
field.value = field.value || [];
field.value.push({});
field.onInput(field.value);
}}
icon={<PlusOutlined />}
>
{/* {t('Add new')} */}
{t('Add new')}
</Button>
)}
{field.componentProps?.allowSelectExistingRecord && (
<Button
type={'text'}
block
className={css`
display: block;
border-radius: 0px;
`}
onClick={() => {
setVisibleSelector(true);
}}
>
{t('Select')}
</Button>
)}
</>
)
}
isSubTable={true}
/>
</FormActiveFieldsProvider>
</FlagProvider>
<ActionContextProvider
value={{
openSize,
openMode: 'drawer',
visible: visibleSelector,
setVisible: setVisibleSelector,
}}
>
<RecordPickerProvider {...pickerProps}>
<CollectionProvider name={collectionField?.target}>
<FormProvider>
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
<SchemaComponentOptions
scope={{
usePickActionProps,
useTableSelectorProps,
useCreateActionProps,
}}
>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema.parent}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.Selector';
}}
/>
</SchemaComponentOptions>
</TableSelectorParamsProvider>
</FormProvider>
</CollectionProvider>
</RecordPickerProvider>
</ActionContextProvider>
</div>
);
},

View File

@ -32,6 +32,72 @@ export const formItemSettings = new SchemaSettings({
name: 'FormItemSettings',
items: [
...(generalSettingsItems as any),
{
name: 'allowAddNewData',
type: 'switch',
useVisible() {
const readPretty = useIsFieldReadPretty();
const isAssociationField = useIsAssociationField();
const fieldMode = useFieldMode();
return !readPretty && isAssociationField && ['SubTable'].includes(fieldMode);
},
useComponentProps() {
const { t } = useTranslation();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { dn, refresh } = useDesignable();
return {
title: t('Allow add new'),
checked: fieldSchema['x-component-props']?.allowAddnew !== (false as boolean),
onChange(value) {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
field.componentProps.allowAddnew = value;
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props'].allowAddnew = value;
schema['x-component-props'] = fieldSchema['x-component-props'];
dn.emit('patch', {
schema,
});
refresh();
},
};
},
},
{
name: 'allowSelectExistingRecord',
type: 'switch',
useVisible() {
const readPretty = useIsFieldReadPretty();
const isAssociationField = useIsAssociationField();
const fieldMode = useFieldMode();
return !readPretty && isAssociationField && ['SubTable'].includes(fieldMode);
},
useComponentProps() {
const { t } = useTranslation();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { dn, refresh } = useDesignable();
return {
title: t('Allow selection of existing records'),
checked: fieldSchema['x-component-props']?.allowSelectExistingRecord,
onChange(value) {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
field.componentProps.allowSelectExistingRecord = value;
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props'].allowSelectExistingRecord = value;
schema['x-component-props'] = fieldSchema['x-component-props'];
dn.emit('patch', {
schema,
});
refresh();
},
};
},
},
{
name: 'quickUpload',
type: 'switch',

View File

@ -266,9 +266,9 @@ test.describe('form item & create form', () => {
// 选择 Sub-form
await (async (page: Page, fieldName: string) => {
await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover();
await page
.getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`)
.hover();
// await page
// .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`)
// .hover();
})(page, 'manyToMany');
await page.getByRole('menuitem', { name: 'Field component' }).click();
await page.getByRole('option', { name: 'Sub-form', exact: true }).click();

View File

@ -419,7 +419,7 @@ test.describe('table column & sub-table in edit form', () => {
],
variableValue: ['Current user', 'Nickname'],
expectVariableValue: async () => {
await page.getByRole('button', { name: 'plus' }).click();
await page.getByRole('button', { name: 'Add new' }).click();
await expect(
page
.getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText')