feat(data-source-manager): filterTargetKey configuration optimization (#4766)

* feat(data-source-manager): filterTargetKey configuration optimization

* fix: bugs

* fix: improve code

* fix: error

* fix: error
This commit is contained in:
chenos 2024-06-28 09:00:16 +08:00 committed by GitHub
parent 7459da6aab
commit a86315de61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 241 additions and 39 deletions

View File

@ -85,9 +85,9 @@ export class SqlCollectionTemplate extends CollectionTemplate {
},
...getConfigurableProperties('category'),
filterTargetKey: {
title: `{{ t("Filter target key")}}`,
title: `{{ t("Record unique key")}}`,
type: 'single',
description: `{{t( "Filter data based on the specific field, with the requirement that the field value must be unique.")}}`,
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],

View File

@ -148,9 +148,9 @@ export class ViewCollectionTemplate extends CollectionTemplate {
},
},
filterTargetKey: {
title: `{{ t("Filter target key")}}`,
title: `{{ t("Record unique key")}}`,
type: 'single',
description: `{{t( "Filter data based on the specific field, with the requirement that the field value must be unique.")}}`,
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],

View File

@ -225,9 +225,15 @@ export class Collection {
return this.options.resource;
}
setOption(key: string, value: any) {
this.options[key] = value;
return this;
}
getOptions() {
return this.options;
}
getOption<K extends keyof CollectionOptions>(key: K): CollectionOptions[K] {
return this.options[key];
}

View File

@ -932,7 +932,9 @@
"Separator": "分隔符",
"Prefix": "前缀",
"Suffix": "后缀",
"Record unique key": "记录唯一标识符",
"Filter target key": "筛选目标键",
"If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.": "当数据表没有主键时,你需要配置记录唯一标识符,用于在区块中定位行记录,不配置将无法创建该表的数据区块。",
"Filter data based on the specific field, with the requirement that the field value must be unique.": "根据特定的字段筛选数据,字段值必须具备唯一性。",
"Multiply by": "乘以",
"Divide by": "除以",

View File

@ -100,7 +100,8 @@ test.describe('create collection', () => {
await collectionSettings.change('Store the last update time of each record', false);
await collectionSettings.submit();
await expect(page.getByRole('cell', { name: collectionDisplayName, exact: true })).toBeVisible();
await expect(page.getByRole('cell', { name: collectionDisplayName })).toBeVisible();
await expect(page.getByRole('cell', { name: collectionName, exact: true })).toBeVisible();
await collectionManagerPage.deleteItem(collectionName);
});

View File

@ -8,34 +8,35 @@
*/
import { createForm, Field } from '@formily/core';
import { FieldContext, FormContext, useField, RecursionField } from '@formily/react';
import { message } from 'antd';
import React, { useContext, useMemo, useEffect, createContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { FieldContext, FormContext, RecursionField, useField } from '@formily/react';
import {
useAPIClient,
SchemaComponentOptions,
useRecord,
useAttach,
Collection,
ResourceActionContext,
ResourceActionProvider,
useResourceContext,
useCompile,
SchemaComponentOptions,
Select,
Collection,
useAPIClient,
useAttach,
useCompile,
useDataSourceManager,
useRecord,
useResourceContext,
} from '@nocobase/client';
import { collection, fieldsTableSchema } from './schema/collectionFields';
import { TitleField } from './components/TitleField';
import { CollectionFieldInterfaceSelect } from './components/CollectionFieldInterfaceSelect';
import { message } from 'antd';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { AddCollectionField } from './AddFieldAction';
import { ForeignKey, SourceCollection, SourceKey, TargetKey, ThroughCollection } from './components';
import { CollectionFieldInterfaceSelect } from './components/CollectionFieldInterfaceSelect';
import { FieldTitleInput } from './components/FieldTitleInput';
import { useBulkDestroyActionAndRefreshCM, useDestroyActionAndRefreshCM } from './hooks';
import { EditCollectionField } from './EditFieldAction';
import { SourceCollection, TargetKey, SourceKey, ForeignKey, ThroughCollection } from './components';
import { FieldType } from './components/FieldType';
import { TitleField } from './components/TitleField';
import { UnSupportFields } from './components/UnSupportFields';
import { EditCollectionField } from './EditFieldAction';
import { FilterTargetKeyAlert } from './FilterTargetKeyAlert';
import { useBulkDestroyActionAndRefreshCM, useDestroyActionAndRefreshCM } from './hooks';
import { collection, fieldsTableSchema } from './schema/collectionFields';
const RemoteCollectionContext = createContext<{
targetCollection: Collection;
@ -186,6 +187,7 @@ export const CollectionFields = () => {
targetCollection,
}}
>
<FilterTargetKeyAlert collectionName={name} />
<ResourceActionProvider {...resourceActionProps}>
<FormContext.Provider value={form}>
<FieldContext.Provider value={f}>

View File

@ -0,0 +1,30 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ExclamationCircleTwoTone } from '@ant-design/icons';
import { useCollectionRecordData, useCompile } from '@nocobase/client';
import { Popover } from 'antd';
import React from 'react';
import { SetFilterTargetKey } from './SetFilterTargetKey';
export const CollectionTitle = () => {
const record = useCollectionRecordData();
const compile = useCompile();
if (record.filterTargetKey) {
return compile(record.title);
}
return (
<div style={{ display: 'inline' }}>
<Popover trigger={['click']} content={<SetFilterTargetKey size={'small'} style={{ width: '20em' }} />}>
<ExclamationCircleTwoTone style={{ marginRight: '0.3em' }} twoToneColor="#faad14" />
</Popover>
{compile(record.title)}
</div>
);
};

View File

@ -10,7 +10,6 @@
import { ArrayTable } from '@formily/antd-v5';
import { ISchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { tval } from '@nocobase/utils/client';
import {
ActionContextProvider,
IField,
@ -18,15 +17,16 @@ import {
SchemaComponent,
useAPIClient,
useActionContext,
useCancelAction,
useCollectionManager_deprecated,
useCompile,
useDataSourceManager,
useRecord,
useRequest,
useResourceActionContext,
useResourceContext,
useCancelAction,
useDataSourceManager,
} from '@nocobase/client';
import { tval } from '@nocobase/utils/client';
import cloneDeep from 'lodash/cloneDeep';
import omit from 'lodash/omit';
import React, { useEffect, useState } from 'react';
@ -69,10 +69,10 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
properties: {
...omit(properties, 'category', 'inherits', 'moreOptions'),
filterTargetKey: {
title: `{{ t("Filter target key",{ ns: "${NAMESPACE}" }) }}`,
title: `{{ t("Record unique key") }}`,
type: 'single',
description: tval(
'Filter data based on the specific field, with the requirement that the field value must be unique.',
'If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.',
{ ns: NAMESPACE },
),
'x-decorator': 'FormItem',

View File

@ -0,0 +1,28 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useApp } from '@nocobase/client';
import { Alert } from 'antd';
import React, { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { SetFilterTargetKey } from './SetFilterTargetKey';
export const FilterTargetKeyAlert = ({ collectionName }) => {
const app = useApp();
const { name: dataSourceKey = 'main' } = useParams();
const collection = useMemo(() => {
const cm = app.getCollectionManager(dataSourceKey);
return cm.getCollection(collectionName);
}, [app, dataSourceKey, collectionName]);
return (
!collection?.filterTargetKey && (
<Alert style={{ marginBottom: 16 }} type="warning" message={<SetFilterTargetKey />} />
)
);
};

View File

@ -0,0 +1,119 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useAPIClient, useApp, useCollectionRecordData, useCompile, useResourceActionContext } from '@nocobase/client';
import { Button, Popconfirm, Select, Space } from 'antd';
import React, { useContext, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useDSMTranslation } from '../../locale';
import { CollectionListContext } from '../MainDataSourceManager/Configuration/CollectionFields';
export const SetFilterTargetKey = (props) => {
const { size, style } = props;
const api = useAPIClient();
const { name: dataSourceKey = 'main' } = useParams();
const record = useCollectionRecordData();
const app = useApp();
const [filterTargetKey, setFilterTargetKey] = useState();
const [title, setTitle] = useState();
const compile = useCompile();
const collection = useMemo(() => {
const cm = app.getCollectionManager(dataSourceKey);
return cm.getCollection(record.name);
}, [app, dataSourceKey, record.name]);
const options = useMemo(() => {
const cm = app.getCollectionManager(dataSourceKey);
const fields = cm.getCollectionFields(record.name);
return fields
.filter((field) => {
if (!field.interface) {
return false;
}
const interfaceOptions = app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface(
field.interface,
);
if (interfaceOptions.titleUsable) {
return true;
}
return false;
})
.map((field) => ({
label: compile(field.uiSchema?.title),
value: field.name,
}));
}, [app, compile, dataSourceKey, record.name]);
const { refresh } = useResourceActionContext();
const ctx = useContext(CollectionListContext);
const { t } = useDSMTranslation();
return (
<div style={{ ...style }}>
{/* Primary keyRecord unique
key */}
{t(
`If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.`,
)}
{size === 'small' ? <br /> : ' '}
<Space.Compact style={{ marginTop: 5 }}>
<Select
onChange={(value, option) => {
setFilterTargetKey(value);
setTitle(option['label']);
}}
placeholder={t('Select field')}
size={'small'}
options={options}
/>
<Popconfirm
placement="bottom"
title={
<div style={{ width: '15em' }}>
{title
? t(
'Are you sure you want to set the "{{title}}" field as a record unique key? This setting cannot be changed after it\'s been set.',
{ title },
)
: t('Please select a field.')}
</div>
}
onConfirm={async () => {
if (!filterTargetKey) {
return;
}
if (dataSourceKey === 'main') {
await api.request({
url: `collections:update?filterByTk=${record.name}`,
method: 'post',
data: {
filterTargetKey,
},
});
} else {
await api.request({
url: `dataSources/${dataSourceKey}/collections:update?filterByTk=${record.name}`,
method: 'post',
data: {
filterTargetKey,
},
});
}
ctx?.refresh?.();
refresh();
// await app.dataSourceManager.getDataSource(dataSourceKey).reload();
collection.setOption('filterTargetKey', filterTargetKey);
}}
>
<Button type={'primary'} size={'small'}>
{t('OK')}
</Button>
</Popconfirm>
</Space.Compact>
</div>
);
};

View File

@ -9,9 +9,10 @@
import { ISchema, Schema } from '@formily/react';
import { uid } from '@formily/shared';
import { CollectionOptions, i18n, useAPIClient } from '@nocobase/client';
import { message } from 'antd';
import { useTranslation } from 'react-i18next';
import { useAPIClient, i18n, CollectionOptions } from '@nocobase/client';
import { CollectionTitle } from '../CollectionTitle';
export const compile = (source) => {
return Schema.compile(source, { t: i18n.t });
@ -30,7 +31,7 @@ export const collection: CollectionOptions = {
uiSchema: {
title: '{{ t("Collection display name") }}',
type: 'number',
'x-component': 'Input',
'x-component': CollectionTitle,
required: true,
},
},

View File

@ -10,9 +10,6 @@
import { css } from '@emotion/css';
import { createForm, Field } from '@formily/core';
import { FieldContext, FormContext, useField } from '@formily/react';
import { Space, Switch, Table, TableColumnProps, Tag, Tooltip, message } from 'antd';
import React, { createContext, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Action,
AddCollectionField,
@ -26,6 +23,7 @@ import {
SchemaComponent,
SyncFieldsAction,
SyncSQLFieldsAction,
useAPIClient,
useAttach,
useBulkDestroyActionAndRefreshCM,
useCollectionManager_deprecated,
@ -36,9 +34,12 @@ import {
useResourceActionContext,
useResourceContext,
ViewCollectionField,
useAPIClient,
} from '@nocobase/client';
import { message, Space, Switch, Table, TableColumnProps, Tag, Tooltip } from 'antd';
import React, { createContext, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FilterTargetKeyAlert } from '../../CollectionsManager/FilterTargetKeyAlert';
import { collection } from './schemas/collectionFields';
const resourceActionProps = {
association: {
@ -59,7 +60,7 @@ const resourceActionProps = {
},
};
const CollectionListContext = createContext(null);
export const CollectionListContext = createContext(null);
const CollectionFieldsProvider = (props) => {
return (
@ -439,6 +440,7 @@ const CollectionFieldsInternal = () => {
return (
<FormContext.Provider value={form}>
<FieldContext.Provider value={f}>
<FilterTargetKeyAlert collectionName={name} />
<Space
align={'end'}
className={css`

View File

@ -9,11 +9,11 @@
import { ISchema, Schema } from '@formily/react';
import { uid } from '@formily/shared';
import type { CollectionOptions } from '@nocobase/client';
import { CollectionCategory, CollectionTemplateTag, i18n, useAPIClient } from '@nocobase/client';
import { message } from 'antd';
import { useTranslation } from 'react-i18next';
import { useAPIClient, i18n } from '@nocobase/client';
import { CollectionCategory, CollectionTemplateTag } from '@nocobase/client';
import type { CollectionOptions } from '@nocobase/client';
import { CollectionTitle } from '../../../CollectionsManager/CollectionTitle';
const compile = (source) => {
return Schema.compile(source, { t: i18n.t });
@ -198,11 +198,12 @@ export const collectionTableSchema: ISchema = {
properties: {
column1: {
type: 'void',
title: '{{t("Collection display name")}}',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
title: {
'x-component': 'CollectionField',
'x-component': CollectionTitle,
'x-read-pretty': true,
},
},

View File

@ -8,9 +8,14 @@
*/
import { i18n } from '@nocobase/client';
import { useTranslation } from 'react-i18next';
export const NAMESPACE = 'data-source-manager';
export function lang(key: string, options = {}) {
return i18n.t(key, { ...options, ns: NAMESPACE });
}
export function useDSMTranslation() {
return useTranslation([NAMESPACE, 'client'], { nsMode: 'fallback' });
}

View File

@ -31,5 +31,10 @@
"Data source synchronization in progress": "数据源同步中",
"Data source synchronization successful": "数据源同步成功",
"Filter target key":"筛选目标键",
"Select field": "选择字段",
"OK": "确定",
"Please select a field.": "请选择一个字段。",
"Are you sure you want to set the \"{{title}}\" field as a record unique key? This setting cannot be changed after it's been set.": "你确定将 “{{title}}” 字段设置为主键吗?设置成功后不可修改。",
"If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.": "当数据表没有主键时,你需要配置记录唯一标识符,用于在区块中定位行记录,不配置将无法创建该表的数据区块。",
"Filter data based on the specific field, with the requirement that the field value must be unique.": "根据特定的字段筛选数据,字段值必须具备唯一性。"
}