feat(data-vi): support multiple data sources (#3743)

* feat(data-vi): support multiple data sources

* chore: update

* chore: new pr

* chore: update

* chore: merge

* fix: bug

* fix: isDBInstance

* fix: fix T-3624

* fix: fix T-3625

* fix: test

* fix: fix T-3659

* fix: fix T-3660

* fix: backend tests

* fix: acl

* fix: fix T-3680

* fix: build
This commit is contained in:
YANG QIA 2024-03-25 14:54:13 +08:00 committed by GitHub
parent d691e4c7e6
commit 088f3977a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 647 additions and 349 deletions

View File

@ -11,6 +11,7 @@ export interface DataSourceOptions {
collections?: CollectionOptions[]; collections?: CollectionOptions[];
errorMessage?: string; errorMessage?: string;
status?: 'loaded' | 'loading-failed' | 'loading' | 'reloading'; status?: 'loaded' | 'loading-failed' | 'loading' | 'reloading';
isDBInstance?: boolean;
} }
export type DataSourceFactory = new (options: DataSourceOptions, dataSourceManager: DataSourceManager) => DataSource; export type DataSourceFactory = new (options: DataSourceOptions, dataSourceManager: DataSourceManager) => DataSource;

View File

@ -56,13 +56,22 @@ export interface GeneralSchemaDesignerProps {
* @default true * @default true
*/ */
draggable?: boolean; draggable?: boolean;
showDataSource?: boolean;
} }
/** /**
* @deprecated use `SchemaToolbar` instead * @deprecated use `SchemaToolbar` instead
*/ */
export const GeneralSchemaDesigner: FC<GeneralSchemaDesignerProps> = (props: any) => { export const GeneralSchemaDesigner: FC<GeneralSchemaDesignerProps> = (props: any) => {
const { disableInitializer, title, template, schemaSettings, contextValue, draggable = true } = props; const {
disableInitializer,
title,
template,
schemaSettings,
contextValue,
draggable = true,
showDataSource = true,
} = props;
const { dn, designable } = useDesignable(); const { dn, designable } = useDesignable();
const field = useField(); const field = useField();
const { t } = useTranslation(); const { t } = useTranslation();
@ -112,7 +121,9 @@ export const GeneralSchemaDesigner: FC<GeneralSchemaDesignerProps> = (props: any
<div className={classNames('general-schema-designer-title', titleCss)}> <div className={classNames('general-schema-designer-title', titleCss)}>
<Space size={2}> <Space size={2}>
<span className={'title-tag'}> <span className={'title-tag'}>
{dataSource ? `${compile(dataSource?.displayName)} > ${compile(title)}` : compile(title)} {showDataSource && dataSource
? `${compile(dataSource?.displayName)} > ${compile(title)}`
: compile(title)}
</span> </span>
{template && ( {template && (
<span className={'title-tag'}> <span className={'title-tag'}>

View File

@ -15,66 +15,72 @@ import {
describe('hooks', () => { describe('hooks', () => {
beforeEach(() => { beforeEach(() => {
vi.spyOn(client, 'useCollectionManager_deprecated').mockReturnValue({ vi.spyOn(client, 'useDataSourceManager').mockReturnValue({
getCollectionFields: (name: string) => getDataSource: () => ({
({ collectionManager: {
orders: [ getCollectionFields: (name: string) =>
{ ({
interface: 'string', orders: [
name: 'name', {
uiSchema: { interface: 'string',
title: '{{t("Name")}}', name: 'name',
}, uiSchema: {
type: 'string', title: '{{t("Name")}}',
}, },
{ type: 'string',
interface: 'number', },
name: 'price', {
uiSchema: { interface: 'number',
title: '{{t("Price")}}', name: 'price',
}, uiSchema: {
type: 'double', title: '{{t("Price")}}',
}, },
{ type: 'double',
interface: 'createdAt', },
name: 'createdAt', {
uiSchema: { interface: 'createdAt',
title: '{{t("Created At")}}', name: 'createdAt',
}, uiSchema: {
type: 'date', title: '{{t("Created At")}}',
}, },
{ type: 'date',
interface: 'm2o', },
name: 'user', {
uiSchema: { interface: 'm2o',
title: '{{t("User")}}', name: 'user',
}, uiSchema: {
target: 'users', title: '{{t("User")}}',
type: 'belongsTo', },
}, target: 'users',
], type: 'belongsTo',
users: [ },
{ ],
interface: 'string', users: [
name: 'name', {
uiSchema: { interface: 'string',
title: '{{t("Name")}}', name: 'name',
}, uiSchema: {
type: 'string', title: '{{t("Name")}}',
}, },
], type: 'string',
})[name], },
getInterface: (i: string) => { ],
switch (i) { })[name],
case 'm2o': },
return { }),
filterable: { collectionFieldInterfaceManager: {
nested: true, getFieldInterface: (i: string) => {
}, switch (i) {
}; case 'm2o':
default: return {
return {}; filterable: {
} nested: true,
},
};
default:
return {};
}
},
}, },
} as any); } as any);
}); });
@ -84,7 +90,7 @@ describe('hooks', () => {
}); });
test('useFieldsWithAssociation', () => { test('useFieldsWithAssociation', () => {
const { result } = renderHook(() => useFieldsWithAssociation('orders')); const { result } = renderHook(() => useFieldsWithAssociation('main', 'orders'));
expect(result.current).toMatchObject([ expect(result.current).toMatchObject([
{ {
key: 'name', key: 'name',
@ -118,7 +124,7 @@ describe('hooks', () => {
}); });
test('useChartFields', () => { test('useChartFields', () => {
const fields = renderHook(() => useFieldsWithAssociation('orders')).result.current; const fields = renderHook(() => useFieldsWithAssociation('main', 'orders')).result.current;
const { result } = renderHook(() => useChartFields(fields)); const { result } = renderHook(() => useChartFields(fields));
const func = result.current; const func = result.current;
const field = { const field = {
@ -155,7 +161,7 @@ describe('hooks', () => {
}); });
test('useFormatters', () => { test('useFormatters', () => {
const fields = renderHook(() => useFieldsWithAssociation('orders')).result.current; const fields = renderHook(() => useFieldsWithAssociation('main', 'orders')).result.current;
const { result } = renderHook(() => useFormatters(fields)); const { result } = renderHook(() => useFormatters(fields));
const func = result.current; const func = result.current;
const field = { const field = {
@ -169,7 +175,7 @@ describe('hooks', () => {
}); });
test('useFieldTypes', () => { test('useFieldTypes', () => {
const fields = renderHook(() => useFieldsWithAssociation('orders')).result.current; const fields = renderHook(() => useFieldsWithAssociation('main', 'orders')).result.current;
const { result } = renderHook(() => useFieldTypes(fields)); const { result } = renderHook(() => useFieldTypes(fields));
const func = result.current; const func = result.current;
let state1 = {}; let state1 = {};
@ -234,7 +240,7 @@ describe('hooks', () => {
}); });
test('useOrderFieldsOptions', () => { test('useOrderFieldsOptions', () => {
const fields = renderHook(() => useFieldsWithAssociation('orders')).result.current; const fields = renderHook(() => useFieldsWithAssociation('main', 'orders')).result.current;
const { result } = renderHook(() => useOrderFieldsOptions([], fields)); const { result } = renderHook(() => useOrderFieldsOptions([], fields));
const func = result.current; const func = result.current;
const field1 = { const field1 = {

View File

@ -10,7 +10,7 @@ import { useChartsTranslation } from '../locale';
export const ChartV2BlockDesigner: React.FC = () => { export const ChartV2BlockDesigner: React.FC = () => {
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
return ( return (
<GeneralSchemaDesigner title={t('Charts')}> <GeneralSchemaDesigner title={t('Charts')} showDataSource={false}>
<SchemaSettingsBlockTitleItem /> <SchemaSettingsBlockTitleItem />
<SchemaSettingsDivider /> <SchemaSettingsDivider />
<SchemaSettingsRemove <SchemaSettingsRemove

View File

@ -2,6 +2,7 @@ import { BarChartOutlined, LineChartOutlined } from '@ant-design/icons';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { import {
CompatibleSchemaInitializer, CompatibleSchemaInitializer,
DEFAULT_DATA_SOURCE_KEY,
DataBlockInitializer, DataBlockInitializer,
SchemaInitializerItem, SchemaInitializerItem,
useACLRoleContext, useACLRoleContext,
@ -15,20 +16,23 @@ import { lang } from '../locale';
const ChartInitializer = () => { const ChartInitializer = () => {
const { setVisible, setCurrent } = useContext(ChartConfigContext); const { setVisible, setCurrent } = useContext(ChartConfigContext);
const { allowAll, parseAction } = useACLRoleContext(); const { parseAction } = useACLRoleContext();
const itemConfig = useSchemaInitializerItem(); const itemConfig = useSchemaInitializerItem();
const filter = useCallback( const filter = useCallback(
(item) => { (item) => {
const params = parseAction(`${item.name}:list`); const params = parseAction(`${item.name}:list`);
return params; return params;
}, },
[allowAll, parseAction], [parseAction],
); );
return ( return (
<DataBlockInitializer <DataBlockInitializer
{...itemConfig} {...itemConfig}
filter={filter} filter={filter}
filterDataSource={(ds) => {
return ds.key === DEFAULT_DATA_SOURCE_KEY || ds.getOptions().isDBInstance;
}}
icon={<BarChartOutlined />} icon={<BarChartOutlined />}
componentType={'Chart'} componentType={'Chart'}
onCreateBlockSchema={async ({ item }) => { onCreateBlockSchema={async ({ item }) => {

View File

@ -2,6 +2,7 @@ import React, { createContext, useState } from 'react';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
type ChartData = { type ChartData = {
dataSource: string;
collection: string; collection: string;
service: any; service: any;
query: any; query: any;
@ -18,8 +19,8 @@ export const ChartDataProvider: React.FC = (props) => {
const [charts, setCharts] = useState<{ const [charts, setCharts] = useState<{
[uid: string]: ChartData; [uid: string]: ChartData;
}>({}); }>({});
const addChart = useMemoizedFn((uid: string, { collection, service, query }: ChartData) => { const addChart = useMemoizedFn((uid: string, { dataSource, collection, service, query }: ChartData) => {
setCharts((charts) => ({ ...charts, [uid]: { collection, service, query } })); setCharts((charts) => ({ ...charts, [uid]: { dataSource, collection, service, query } }));
}); });
const removeChart = useMemoizedFn((uid: string) => { const removeChart = useMemoizedFn((uid: string) => {
setCharts((charts) => ({ ...charts, [uid]: undefined })); setCharts((charts) => ({ ...charts, [uid]: undefined }));

View File

@ -34,14 +34,14 @@ export class Table extends AntdChart {
}); });
const pageSize = advanced?.pagination?.pageSize || 10; const pageSize = advanced?.pagination?.pageSize || 10;
return { return {
bordered: true, // bordered: true,
size: 'middle', size: 'middle',
// pagination: pagination:
// dataSource.length < pageSize dataSource.length < pageSize
// ? false ? false
// : { : {
// pageSize, pageSize,
// }, },
dataSource, dataSource,
columns, columns,
scroll: { scroll: {

View File

@ -35,11 +35,11 @@ export class G2PlotChart extends Chart {
}, },
}, },
tooltip: (d, index: number, data, column: any) => { tooltip: (d, index: number, data, column: any) => {
const field = column.y.field; const field = column.y?.field;
const props = fieldProps[field]; const props = fieldProps[field];
const name = props?.label || field; const name = props?.label || field;
const transformer = props?.transformer; const transformer = props?.transformer;
const value = column.y.value[index]; const value = column.y?.value[index];
return { name, value: transformer ? transformer(value) : value }; return { name, value: transformer ? transformer(value) : value };
}, },
axis: { axis: {

View File

@ -2,21 +2,15 @@ import { RightSquareOutlined } from '@ant-design/icons';
import { ArrayItems, Editable, FormCollapse, FormItem, FormLayout, Switch } from '@formily/antd-v5'; import { ArrayItems, Editable, FormCollapse, FormItem, FormLayout, Switch } from '@formily/antd-v5';
import { Form as FormType, ObjectField, createForm, onFieldChange, onFormInit } from '@formily/core'; import { Form as FormType, ObjectField, createForm, onFieldChange, onFormInit } from '@formily/core';
import { FormConsumer, ISchema, Schema } from '@formily/react'; import { FormConsumer, ISchema, Schema } from '@formily/react';
import { import { AutoComplete, FormProvider, SchemaComponent, gridRowColWrap, useDesignable } from '@nocobase/client';
AutoComplete,
FormProvider,
SchemaComponent,
gridRowColWrap,
useCollectionFieldsOptions,
useCollectionFilterOptions,
useDesignable,
} from '@nocobase/client';
import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography } from 'antd'; import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography } from 'antd';
import { cloneDeep, isEqual } from 'lodash'; import { cloneDeep, isEqual } from 'lodash';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import { import {
useChartFields, useChartFields,
useCollectionOptions, useCollectionOptions,
useCollectionFieldsOptions,
useCollectionFilterOptions,
useData, useData,
useFieldTypes, useFieldTypes,
useFieldsWithAssociation, useFieldsWithAssociation,
@ -57,13 +51,13 @@ export const ChartConfigure: React.FC<{
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
const { service } = useContext(ChartRendererContext); const { service } = useContext(ChartRendererContext);
const { visible, setVisible, current } = useContext(ChartConfigContext); const { visible, setVisible, current } = useContext(ChartConfigContext);
const { schema, field, collection, initialValues } = current || {}; const { schema, field, dataSource, collection, initialValues } = current || {};
const { dn } = useDesignable(); const { dn } = useDesignable();
const { modal } = App.useApp(); const { modal } = App.useApp();
const { insert } = props; const { insert } = props;
const charts = useCharts(); const charts = useCharts();
const fields = useFieldsWithAssociation(collection); const fields = useFieldsWithAssociation(dataSource, collection);
const initChart = (overwrite = false) => { const initChart = (overwrite = false) => {
if (!form.modified) { if (!form.modified) {
return; return;
@ -108,15 +102,19 @@ export const ChartConfigure: React.FC<{
const form = useMemo( const form = useMemo(
() => () =>
createForm({ createForm({
values: { config: { chartType }, ...(initialValues || field?.decoratorProps), collection }, values: {
config: { chartType },
...(initialValues || field?.decoratorProps),
collection: [dataSource, collection],
},
effects: (form) => { effects: (form) => {
onFieldChange('config.chartType', () => initChart(true)); onFieldChange('config.chartType', () => initChart(true));
onFormInit(() => queryReact(form)); onFormInit(() => queryReact(form));
}, },
}), }),
// visible, collection added here to re-initialize form when visible, collection change // visible, dataSource, collection added here to re-initialize form when visible, dataSource, collection change
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[field, visible, collection], [field, visible, dataSource, collection],
); );
const RunButton: React.FC = () => ( const RunButton: React.FC = () => (
@ -133,7 +131,7 @@ export const ChartConfigure: React.FC<{
} }
try { try {
await service.runAsync(collection, form.values.query, true); await service.runAsync(dataSource, collection, form.values.query, true);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
@ -163,7 +161,7 @@ export const ChartConfigure: React.FC<{
const { query, config, transform, mode } = form.values; const { query, config, transform, mode } = form.values;
const afterSave = () => { const afterSave = () => {
setVisible(false); setVisible(false);
current.service?.run(collection, query); current.service?.run(dataSource, collection, query);
queryRef.current.scrollTop = 0; queryRef.current.scrollTop = 0;
configRef.current.scrollTop = 0; configRef.current.scrollTop = 0;
service.mutate(undefined); service.mutate(undefined);
@ -171,6 +169,7 @@ export const ChartConfigure: React.FC<{
const rendererProps = { const rendererProps = {
query, query,
config, config,
dataSource,
collection, collection,
transform, transform,
mode: mode || 'builder', mode: mode || 'builder',
@ -308,19 +307,20 @@ ChartConfigure.Query = function Query() {
const useFormatterOptions = useFormatters(fields); const useFormatterOptions = useFormatters(fields);
const collectionOptions = useCollectionOptions(); const collectionOptions = useCollectionOptions();
const { current, setCurrent } = useContext(ChartConfigContext); const { current, setCurrent } = useContext(ChartConfigContext);
const { collection } = current || {}; const { dataSource, collection } = current || {};
const fieldOptions = useCollectionFieldsOptions(collection, 1); const fieldOptions = useCollectionFieldsOptions(dataSource, collection, 1);
const compiledFieldOptions = Schema.compile(fieldOptions, { t }); const compiledFieldOptions = Schema.compile(fieldOptions, { t });
const filterOptions = useCollectionFilterOptions(collection); const filterOptions = useCollectionFilterOptions(dataSource, collection);
const { service } = useContext(ChartRendererContext); const { service } = useContext(ChartRendererContext);
const onCollectionChange = (value: string) => { const onCollectionChange = (value: string[]) => {
const { schema, field } = current; const { schema, field } = current;
const [dataSource, collection] = value;
setCurrent({ setCurrent({
schema, schema,
field, field,
collection: value, collection,
dataSource: current.dataSource, dataSource,
service: current.service, service: current.service,
initialValues: {}, initialValues: {},
data: undefined, data: undefined,

View File

@ -150,9 +150,9 @@ export const querySchema: ISchema = {
title: '{{t("Collection")}}', title: '{{t("Collection")}}',
type: 'string', type: 'string',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Cascader',
enum: '{{ collectionOptions }}',
'x-component-props': { 'x-component-props': {
options: '{{ collectionOptions }}',
onChange: '{{ onCollectionChange }}', onChange: '{{ onCollectionChange }}',
placeholder: '{{t("Collection")}}', placeholder: '{{t("Collection")}}',
}, },

View File

@ -0,0 +1,9 @@
import React from 'react';
import { InitializerWithSwitch, useSchemaInitializerItem } from '@nocobase/client';
import { ISchema } from '@formily/react';
export const CollectionFieldInitializer = () => {
const schema: ISchema = {};
const itemConfig = useSchemaInitializerItem();
return <InitializerWithSwitch {...itemConfig} item={itemConfig} schema={schema} type={'name'} />;
};

View File

@ -5,7 +5,7 @@ import { useChartsTranslation } from '../locale';
export const ChartFilterBlockDesigner: React.FC = () => { export const ChartFilterBlockDesigner: React.FC = () => {
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
return ( return (
<GeneralSchemaDesigner disableInitializer title={t('Filter')}> <GeneralSchemaDesigner disableInitializer title={t('Filter')} showDataSource={false}>
{/* <SchemaSettings.BlockTitleItem /> */} {/* <SchemaSettings.BlockTitleItem /> */}
{/* <SchemaSettings.Divider /> */} {/* <SchemaSettings.Divider /> */}
<SchemaSettingsRemove <SchemaSettingsRemove

View File

@ -16,6 +16,7 @@ import { ChartFilterCheckbox } from './FilterCheckbox';
import { ArrayItems } from '@formily/antd-v5'; import { ArrayItems } from '@formily/antd-v5';
import { ChartFilterFormItem } from './FilterItemInitializers'; import { ChartFilterFormItem } from './FilterItemInitializers';
import { ChartFilterForm } from './FilterForm'; import { ChartFilterForm } from './FilterForm';
import { CollectionFieldInitializer } from './CollectionFieldInitializer';
export const ChartFilterBlockProvider: React.FC = (props) => { export const ChartFilterBlockProvider: React.FC = (props) => {
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
@ -43,6 +44,7 @@ export const ChartFilterBlockProvider: React.FC = (props) => {
ArrayItems, ArrayItems,
ChartFilterCollapseDesigner, ChartFilterCollapseDesigner,
ChartFilterActionDesigner, ChartFilterActionDesigner,
CollectionFieldInitializer,
}} }}
scope={{ t, useChartFilterActionProps, useChartFilterResetProps, useChartFilterCollapseProps }} scope={{ t, useChartFilterActionProps, useChartFilterResetProps, useChartFilterCollapseProps }}
> >

View File

@ -1,7 +1,7 @@
import React, { memo, useContext, useEffect, useMemo, useRef } from 'react'; import React, { memo, useContext, useEffect, useMemo, useRef } from 'react';
import { createForm, onFieldInit, onFieldMount, onFieldUnmount } from '@formily/core'; import { createForm, onFieldInit, onFieldMount, onFieldUnmount } from '@formily/core';
import { ChartFilterContext } from './FilterProvider'; import { ChartFilterContext } from './FilterProvider';
import { FormV2, VariablesContext } from '@nocobase/client'; import { DEFAULT_DATA_SOURCE_KEY, FormV2, VariablesContext } from '@nocobase/client';
import { setDefaultValue } from './utils'; import { setDefaultValue } from './utils';
import { useChartFilter } from '../hooks'; import { useChartFilter } from '../hooks';
@ -33,7 +33,10 @@ export const ChartFilterForm: React.FC = memo((props) => {
if (!name) { if (!name) {
return; return;
} }
setField(name, { title: field.title, operator: field.componentProps['filter-operator'] }); setField(name, {
title: field.title,
operator: field.componentProps['filter-operator'],
});
// parse field title // parse field title
if (field.title.includes('/')) { if (field.title.includes('/')) {

View File

@ -13,6 +13,7 @@ import {
useDesignable, useDesignable,
SchemaSettingsSelectItem, SchemaSettingsSelectItem,
CollectionFieldOptions_deprecated, CollectionFieldOptions_deprecated,
DEFAULT_DATA_SOURCE_KEY,
} from '@nocobase/client'; } from '@nocobase/client';
import { useChartsTranslation } from '../locale'; import { useChartsTranslation } from '../locale';
import { Schema, useField, useFieldSchema } from '@formily/react'; import { Schema, useField, useFieldSchema } from '@formily/react';
@ -21,7 +22,7 @@ import _ from 'lodash';
import { ChartFilterContext } from './FilterProvider'; import { ChartFilterContext } from './FilterProvider';
import { getPropsSchemaByComponent, setDefaultValue } from './utils'; import { getPropsSchemaByComponent, setDefaultValue } from './utils';
import { ChartFilterVariableInput } from './FilterVariableInput'; import { ChartFilterVariableInput } from './FilterVariableInput';
import { useChartFilter, useCollectionJoinFieldTitle } from '../hooks'; import { useChartDataSource, useChartFilter, useCollectionJoinFieldTitle } from '../hooks';
import { Typography } from 'antd'; import { Typography } from 'antd';
import { getFormulaInterface } from '../utils'; import { getFormulaInterface } from '../utils';
const { Text } = Typography; const { Text } = Typography;
@ -72,29 +73,33 @@ const EditTitle = () => {
const EditOperator = () => { const EditOperator = () => {
const compile = useCompile(); const compile = useCompile();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const fieldName = fieldSchema.name as string;
const field = useField<Field>(); const field = useField<Field>();
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
const { dn } = useDesignable(); const { dn } = useDesignable();
const { setField } = useContext(ChartFilterContext); const { setField } = useContext(ChartFilterContext);
const { getInterface, getCollectionJoinField } = useCollectionManager_deprecated(); const fieldName = fieldSchema['x-collection-field'];
const dataSource = fieldSchema['x-data-source'] || DEFAULT_DATA_SOURCE_KEY;
const { cm, fim } = useChartDataSource(dataSource);
if (!cm) {
return null;
}
const getOperators = (props: CollectionFieldOptions_deprecated) => { const getOperators = (props: CollectionFieldOptions_deprecated) => {
let fieldInterface = props?.interface; let fieldInterface = props?.interface;
if (fieldInterface === 'formula') { if (fieldInterface === 'formula') {
fieldInterface = getFormulaInterface(props.dataType) || props.dataType; fieldInterface = getFormulaInterface(props.dataType) || props.dataType;
} }
const interfaceConfig = getInterface(fieldInterface); const interfaceConfig = fim.getFieldInterface(fieldInterface);
const operatorList = interfaceConfig?.filterable?.operators || []; const operatorList = interfaceConfig?.filterable?.operators || [];
return { operatorList, interfaceConfig }; return { operatorList, interfaceConfig };
}; };
let props = getCollectionJoinField(fieldName); let props = cm.getCollectionField(fieldName);
let { operatorList, interfaceConfig } = getOperators(props); let { operatorList, interfaceConfig } = getOperators(props);
if (!operatorList.length) { if (!operatorList.length) {
const names = fieldName.split('.'); const names = fieldName.split('.');
const name = names.pop(); const name = names.pop();
props = getCollectionJoinField(names.join('.')); props = cm.getCollectionField(names.join('.'));
if (!props) { if (!props) {
return null; return null;
} }
@ -159,7 +164,7 @@ const EditOperator = () => {
setOperatorComponent(operator, defaultComponent); setOperatorComponent(operator, defaultComponent);
} }
setField(fieldName, { operator }); setField(fieldSchema.name as string, { operator });
dn.refresh(); dn.refresh();
}} }}
/> />
@ -300,10 +305,11 @@ export const ChartFilterItemDesigner: React.FC = () => {
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const fieldName = fieldSchema.name as string; const fieldName = fieldSchema.name as string;
const dataSource = fieldSchema['x-data-source'] || DEFAULT_DATA_SOURCE_KEY;
const collectionField = getField(fieldName) || getCollectionJoinField(fieldSchema['x-collection-field']); const collectionField = getField(fieldName) || getCollectionJoinField(fieldSchema['x-collection-field']);
const isCustom = fieldName.startsWith('custom.'); const isCustom = fieldName.startsWith('custom.');
const hasProps = getPropsSchemaByComponent(fieldSchema['x-component']); const hasProps = getPropsSchemaByComponent(fieldSchema['x-component']);
const originalTitle = useCollectionJoinFieldTitle(fieldName); const originalTitle = useCollectionJoinFieldTitle(dataSource, fieldName);
return ( return (
<GeneralSchemaDesigner disableInitializer> <GeneralSchemaDesigner disableInitializer>
{!isCustom && ( {!isCustom && (

View File

@ -7,7 +7,9 @@ import {
ACLCollectionFieldProvider, ACLCollectionFieldProvider,
BlockItem, BlockItem,
CollectionFieldProvider, CollectionFieldProvider,
CollectionManagerProvider,
CollectionProvider, CollectionProvider,
DEFAULT_DATA_SOURCE_KEY,
CompatibleSchemaInitializer, CompatibleSchemaInitializer,
FormDialog, FormDialog,
HTMLEncode, HTMLEncode,
@ -16,6 +18,7 @@ import {
SchemaInitializerItem, SchemaInitializerItem,
gridRowColWrap, gridRowColWrap,
useCollectionManager_deprecated, useCollectionManager_deprecated,
useDataSourceManager,
useDesignable, useDesignable,
useGlobalTheme, useGlobalTheme,
useSchemaInitializerItem, useSchemaInitializerItem,
@ -50,6 +53,7 @@ const ErrorFallback = ({ error }) => {
export const ChartFilterFormItem = observer( export const ChartFilterFormItem = observer(
(props: any) => { (props: any) => {
const { t } = useChartsTranslation();
const field = useField<Field>(); const field = useField<Field>();
const schema = useFieldSchema(); const schema = useFieldSchema();
const showTitle = schema['x-decorator-props']?.showTitle ?? true; const showTitle = schema['x-decorator-props']?.showTitle ?? true;
@ -80,21 +84,31 @@ export const ChartFilterFormItem = observer(
}, },
); );
}, [showTitle]); }, [showTitle]);
const dataSource = schema?.['x-data-source'] || DEFAULT_DATA_SOURCE_KEY;
const collectionField = schema?.['x-collection-field'] || ''; const collectionField = schema?.['x-collection-field'] || '';
const [collection] = collectionField.split('.'); const [collection] = collectionField.split('.');
// const { getIsChartCollectionExists } = useChartData();
// const exists = (schema.name as string).startsWith('custom.') || getIsChartCollectionExists(dataSource, collection);
return ( return (
<CollectionProvider name={collection} allowNull={!collection}> <BlockItem className={'nb-form-item'}>
<CollectionFieldProvider name={schema.name} allowNull={!schema['x-collection-field']}> <CollectionManagerProvider dataSource={dataSource}>
<ACLCollectionFieldProvider> <CollectionProvider name={collection} allowNull={!collection}>
<BlockItem className={'nb-form-item'}> <CollectionFieldProvider name={schema.name} allowNull={!schema['x-collection-field']}>
<ErrorBoundary onError={(err) => console.log(err)} FallbackComponent={ErrorFallback}> <ACLCollectionFieldProvider>
<FormItem className={className} {...props} extra={extra} /> {/* {exists ? ( */}
</ErrorBoundary> <ErrorBoundary onError={(err) => console.log(err)} FallbackComponent={ErrorFallback}>
</BlockItem> <FormItem className={className} {...props} extra={extra} />
</ACLCollectionFieldProvider> </ErrorBoundary>
</CollectionFieldProvider> {/* ) : ( */}
</CollectionProvider> {/* <div style={{ color: '#ccc', marginBottom: '10px' }}> */}
{/* {t('The chart using the collection of this field have been deleted. Please remove this field.')} */}
{/* </div> */}
{/* )} */}
</ACLCollectionFieldProvider>
</CollectionFieldProvider>
</CollectionProvider>
</CollectionManagerProvider>
</BlockItem>
); );
}, },
{ displayName: 'ChartFilterFormItem' }, { displayName: 'ChartFilterFormItem' },
@ -110,7 +124,7 @@ export const ChartFilterCustomItemInitializer: React.FC<{
const { theme } = useGlobalTheme(); const { theme } = useGlobalTheme();
const { insert } = props; const { insert } = props;
const itemConfig = useSchemaInitializerItem(); const itemConfig = useSchemaInitializerItem();
const { getCollectionJoinField, getInterface } = useCollectionManager_deprecated(); const dm = useDataSourceManager();
const sourceFields = useChartFilterSourceFields(); const sourceFields = useChartFilterSourceFields();
const { options: fieldComponents, values: fieldComponentValues } = useFieldComponents(); const { options: fieldComponents, values: fieldComponentValues } = useFieldComponents();
const handleClick = useCallback(async () => { const handleClick = useCallback(async () => {
@ -177,8 +191,17 @@ export const ChartFilterCustomItemInitializer: React.FC<{
}, },
effects() { effects() {
onFieldValueChange('source', (field) => { onFieldValueChange('source', (field) => {
const name = field.value?.join('.'); if (!field.value) {
const props = getCollectionJoinField(name); return;
}
const [dataSource, ...fields] = field.value;
const ds = dm.getDataSource(dataSource);
if (!ds) {
return;
}
const cm = ds.collectionManager;
const name = fields.join('.');
const props = cm.getCollectionField(name);
if (!props) { if (!props) {
return; return;
} }
@ -204,7 +227,8 @@ export const ChartFilterCustomItemInitializer: React.FC<{
}, },
}); });
const { name, title, component, props } = values; const { name, title, component, props } = values;
const defaultSchema = getInterface(component)?.default?.uiSchema || {}; const fim = dm.collectionFieldInterfaceManager;
const defaultSchema = fim.getFieldInterface(component)?.default?.uiSchema || {};
insert( insert(
gridRowColWrap({ gridRowColWrap({
'x-component': component, 'x-component': component,
@ -227,11 +251,8 @@ export const ChartFilterCustomItemInitializer: React.FC<{
}); });
ChartFilterCustomItemInitializer.displayName = 'ChartFilterCustomItemInitializer'; ChartFilterCustomItemInitializer.displayName = 'ChartFilterCustomItemInitializer';
/** const filterItemInitializers = {
* @deprecated name: 'chartFilterForm:configureFields',
*/
export const chartFilterItemInitializers_deprecated = new CompatibleSchemaInitializer({
name: 'ChartFilterItemInitializers',
'data-testid': 'configure-fields-button-of-chart-filter-item', 'data-testid': 'configure-fields-button-of-chart-filter-item',
wrap: gridRowColWrap, wrap: gridRowColWrap,
icon: 'SettingOutlined', icon: 'SettingOutlined',
@ -242,23 +263,34 @@ export const chartFilterItemInitializers_deprecated = new CompatibleSchemaInitia
name: 'displayFields', name: 'displayFields',
title: '{{ t("Display fields") }}', title: '{{ t("Display fields") }}',
useChildren: () => { useChildren: () => {
const { getCollection } = useCollectionManager_deprecated(); const { t } = useChartsTranslation();
const { getChartCollections } = useChartData(); const { chartCollections, showDataSource } = useChartData();
const { getChartFilterFields } = useChartFilter(); const { getChartFilterFields } = useChartFilter();
const collections = getChartCollections(); const dm = useDataSourceManager();
const fim = dm.collectionFieldInterfaceManager;
return useMemo(() => { return useMemo(() => {
return collections.map((name: any) => { const options = Object.entries(chartCollections).map(([dataSource, collections]) => {
const collection = getCollection(name); const ds = dm.getDataSource(dataSource);
const fields = getChartFilterFields(collection);
return { return {
name: collection.key, name: ds.key,
title: Schema.compile(ds.displayName, { t }),
type: 'subMenu', type: 'subMenu',
title: collection.title, children: collections.map((name) => {
children: fields, const cm = ds.collectionManager;
const collection = cm.getCollection(name);
const fields = getChartFilterFields({ dataSource, collection, cm, fim });
return {
name: collection.key,
title: Schema.compile(collection.title, { t }),
type: 'subMenu',
children: fields,
};
}),
}; };
}); });
}, [collections]); return showDataSource ? options : options[0]?.children || [];
}, [chartCollections, showDataSource]);
}, },
}, },
{ {
@ -275,54 +307,17 @@ export const chartFilterItemInitializers_deprecated = new CompatibleSchemaInitia
}, },
}, },
], ],
};
/**
* @deprecated
*/
export const chartFilterItemInitializers_deprecated = new CompatibleSchemaInitializer({
...filterItemInitializers,
name: 'ChartFilterItemInitializers',
}); });
export const chartFilterItemInitializers = new CompatibleSchemaInitializer( export const chartFilterItemInitializers = new CompatibleSchemaInitializer(
{ filterItemInitializers,
name: 'chartFilterForm:configureFields',
'data-testid': 'configure-fields-button-of-chart-filter-item',
wrap: gridRowColWrap,
icon: 'SettingOutlined',
title: '{{ t("Configure fields") }}',
items: [
{
type: 'itemGroup',
name: 'displayFields',
title: '{{ t("Display fields") }}',
useChildren: () => {
const { getCollection } = useCollectionManager_deprecated();
const { getChartCollections } = useChartData();
const { getChartFilterFields } = useChartFilter();
const collections = getChartCollections();
return useMemo(() => {
return collections.map((name: any) => {
const collection = getCollection(name);
const fields = getChartFilterFields(collection);
return {
name: collection.key,
type: 'subMenu',
title: collection.title,
children: fields,
};
});
}, [collections]);
},
},
{
name: 'divider',
type: 'divider',
},
{
name: 'custom',
type: 'item',
title: lang('Custom'),
Component: () => {
const { insertAdjacent } = useDesignable();
return <ChartFilterCustomItemInitializer insert={(s: Schema) => insertAdjacent('beforeEnd', s)} />;
},
},
],
},
chartFilterItemInitializers_deprecated, chartFilterItemInitializers_deprecated,
); );

View File

@ -1,20 +1,22 @@
import React, { createContext, useEffect, useState } from 'react'; import React, { createContext, useEffect, useState } from 'react';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
type FilterField = {
title?: string;
operator?: {
value: string;
noValue?: boolean;
};
};
export const ChartFilterContext = createContext<{ export const ChartFilterContext = createContext<{
ready: boolean; ready: boolean;
enabled: boolean; enabled: boolean;
setEnabled: (enabled: boolean) => void; setEnabled: (enabled: boolean) => void;
fields: { fields: {
[name: string]: { [name: string]: FilterField;
title: string;
operator?: {
value: string;
noValue?: boolean;
};
};
}; };
setField: (name: string, field: { title?: string; operator?: string }) => void; setField: (name: string, field: FilterField) => void;
removeField: (name: string) => void; removeField: (name: string) => void;
collapse: { collapse: {
collapsed: boolean; collapsed: boolean;
@ -32,7 +34,7 @@ export const ChartFilterProvider: React.FC = (props) => {
const [fields, setFields] = useState({}); const [fields, setFields] = useState({});
const [collapse, _setCollapse] = useState({ collapsed: false, row: 1 }); const [collapse, _setCollapse] = useState({ collapsed: false, row: 1 });
const [form, _setForm] = useState<any>(); const [form, _setForm] = useState<any>();
const setField = useMemoizedFn((name: string, props: { title?: string; operator?: string }) => { const setField = useMemoizedFn((name: string, props: FilterField) => {
setFields((fields) => ({ setFields((fields) => ({
...fields, ...fields,
[name]: { [name]: {

View File

@ -1,5 +1,7 @@
import { DEFAULT_DATA_SOURCE_KEY } from '@nocobase/client';
import { moment2str } from '@nocobase/utils/client'; import { moment2str } from '@nocobase/utils/client';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Schema } from '@formily/react';
export const getOptionsSchema = () => { export const getOptionsSchema = () => {
const options = { const options = {
@ -175,3 +177,39 @@ export const setDefaultValue = async (field: any, variables: any) => {
field.loading = false; field.loading = false;
} }
}; };
export const FILTER_FIELD_PREFIX_SEPARATOR = '-';
export const getFilterFieldPrefix = (dataSource: string, fieldName: string) => {
return dataSource ? `${dataSource}${FILTER_FIELD_PREFIX_SEPARATOR}${fieldName}` : fieldName;
};
// [dataSource-]collection.fieldName.associateName
export const parseFilterFieldName = (name: string) => {
const [prefix, fieldName] = name.split(FILTER_FIELD_PREFIX_SEPARATOR);
if (fieldName) {
return { dataSource: prefix, fieldName };
}
return { dataSource: DEFAULT_DATA_SOURCE_KEY, fieldName: prefix };
};
export const findSchema = (schema: Schema, key: string, targetName: string) => {
if (!Schema.isSchemaInstance(schema)) return null;
return schema.reduceProperties((buf, s) => {
let fieldName = s[key];
if (!fieldName.includes(FILTER_FIELD_PREFIX_SEPARATOR)) {
fieldName = `${DEFAULT_DATA_SOURCE_KEY}${FILTER_FIELD_PREFIX_SEPARATOR}${fieldName}`;
}
if (fieldName === targetName) {
return s;
}
if (s['x-component'] !== 'Action.Container' && s['x-component'] !== 'AssociationField.Viewer') {
const c = findSchema(s, key, targetName);
if (c) {
return c;
}
}
return buf;
});
};

View File

@ -1,7 +1,16 @@
import { SchemaInitializerItemType, i18n, useActionContext, useCollectionManager_deprecated } from '@nocobase/client'; import {
import { useContext, useMemo } from 'react'; Collection,
CollectionFieldInterfaceManager,
CollectionFieldOptions,
CollectionManager,
SchemaInitializerItemType,
i18n,
useActionContext,
useCollectionManager_deprecated,
useDataSourceManager,
} from '@nocobase/client';
import { useCallback, useContext, useMemo } from 'react';
import { ChartDataContext } from '../block/ChartDataProvider'; import { ChartDataContext } from '../block/ChartDataProvider';
import { CollectionOptions } from '@nocobase/database';
import { Schema } from '@formily/react'; import { Schema } from '@formily/react';
import { useChartsTranslation } from '../locale'; import { useChartsTranslation } from '../locale';
import { ChartFilterContext } from '../filter/FilterProvider'; import { ChartFilterContext } from '../filter/FilterProvider';
@ -10,6 +19,7 @@ import { parse } from '@nocobase/utils/client';
import lodash from 'lodash'; import lodash from 'lodash';
import { getFormulaComponent, getValuesByPath } from '../utils'; import { getFormulaComponent, getValuesByPath } from '../utils';
import deepmerge from 'deepmerge'; import deepmerge from 'deepmerge';
import { findSchema, getFilterFieldPrefix, parseFilterFieldName } from '../filter/utils';
export const useCustomFieldInterface = () => { export const useCustomFieldInterface = () => {
const { getInterface } = useCollectionManager_deprecated(); const { getInterface } = useCollectionManager_deprecated();
@ -44,35 +54,63 @@ export const useCustomFieldInterface = () => {
export const useChartData = () => { export const useChartData = () => {
const { charts } = useContext(ChartDataContext); const { charts } = useContext(ChartDataContext);
const getChartCollections = () => const chartCollections: {
Array.from( [dataSource: string]: string[];
new Set( } = useMemo(() => {
Object.values(charts) return Object.values(charts)
.filter((chart) => chart) .filter((chart) => chart)
.map((chart) => chart.collection), .reduce((mp, chart) => {
), const { dataSource, collection } = chart;
); if (mp[dataSource]?.includes(collection)) {
return mp;
}
mp[dataSource] = [...(mp[dataSource] || []), collection];
return mp;
}, {});
}, [charts]);
const showDataSource = useMemo(() => {
return Object.keys(chartCollections).length > 1;
}, [chartCollections]);
const getIsChartCollectionExists = useCallback(
(dataSource: string, collection: string) => {
return chartCollections[dataSource]?.includes(collection) || false;
},
[chartCollections],
);
return { return {
getChartCollections, chartCollections,
showDataSource,
getIsChartCollectionExists,
}; };
}; };
export const useChartFilter = () => { export const useChartFilter = () => {
const dm = useDataSourceManager();
const { charts } = useContext(ChartDataContext); const { charts } = useContext(ChartDataContext);
const { fieldSchema } = useActionContext(); const { fieldSchema } = useActionContext();
const action = fieldSchema?.['x-action']; const action = fieldSchema?.['x-action'];
const { getCollection, getInterface, getCollectionFields, getCollectionJoinField } =
useCollectionManager_deprecated();
const { fields: fieldProps, form } = useContext(ChartFilterContext); const { fields: fieldProps, form } = useContext(ChartFilterContext);
const getChartFilterFields = (collection: CollectionOptions) => { const getChartFilterFields = ({
const fields = getCollectionFields(collection); dataSource,
const field2item = (field: any, title: string, name: string) => { collection,
cm,
fim,
}: {
dataSource: string;
collection: Collection;
cm: CollectionManager;
fim: CollectionFieldInterfaceManager;
}) => {
const fields = cm.getCollectionFields(collection.name);
const field2item = (field: any, title: string, name: string, fieldName: string) => {
const fieldTitle = field.uiSchema?.title || field.name; const fieldTitle = field.uiSchema?.title || field.name;
const interfaceConfig = getInterface(field.interface); const interfaceConfig = fim.getFieldInterface(field.interface);
const defaultOperator = interfaceConfig?.filterable?.operators?.[0]; const defaultOperator = interfaceConfig?.filterable?.operators?.[0];
const targetCollection = getCollection(field.target); const targetCollection = cm.getCollection(field.target);
title = title ? `${title} / ${fieldTitle}` : fieldTitle; title = title ? `${title} / ${fieldTitle}` : fieldTitle;
let schema = { let schema = {
type: 'string', type: 'string',
@ -82,7 +120,8 @@ export const useChartFilter = () => {
'x-designer': 'ChartFilterItemDesigner', 'x-designer': 'ChartFilterItemDesigner',
'x-component': 'CollectionField', 'x-component': 'CollectionField',
'x-decorator': 'ChartFilterFormItem', 'x-decorator': 'ChartFilterFormItem',
'x-collection-field': `${name}.${field.name}`, 'x-data-source': dataSource,
'x-collection-field': `${fieldName}.${field.name}`,
'x-component-props': { 'x-component-props': {
...field.uiSchema?.['x-component-props'], ...field.uiSchema?.['x-component-props'],
'filter-operator': defaultOperator, 'filter-operator': defaultOperator,
@ -104,6 +143,7 @@ export const useChartFilter = () => {
type: 'item', type: 'item',
title: field?.uiSchema?.title || field.name, title: field?.uiSchema?.title || field.name,
Component: 'CollectionFieldInitializer', Component: 'CollectionFieldInitializer',
find: findSchema,
remove: (schema, cb) => { remove: (schema, cb) => {
cb(schema, { cb(schema, {
breakRemoveOn: { breakRemoveOn: {
@ -126,7 +166,7 @@ export const useChartFilter = () => {
return resultItem; return resultItem;
}; };
const children2item = (child: any, title: string, name: string) => { const children2item = (child: any, title: string, name: string, fieldName: string) => {
const childTitle = child.uiSchema?.title || child.name; const childTitle = child.uiSchema?.title || child.name;
title = title ? `${title} / ${childTitle}` : childTitle; title = title ? `${title} / ${childTitle}` : childTitle;
const defaultOperator = child.operators[0]; const defaultOperator = child.operators[0];
@ -136,7 +176,8 @@ export const useChartFilter = () => {
required: false, required: false,
'x-designer': 'ChartFilterItemDesigner', 'x-designer': 'ChartFilterItemDesigner',
'x-decorator': 'ChartFilterFormItem', 'x-decorator': 'ChartFilterFormItem',
'x-collection-field': `${name}.${child.name}`, 'x-data-source': dataSource,
'x-collection-field': `${fieldName}.${child.name}`,
...child.schema, ...child.schema,
title, title,
'x-component-props': { 'x-component-props': {
@ -159,6 +200,7 @@ export const useChartFilter = () => {
type: 'item', type: 'item',
title: child.title || child.name, title: child.title || child.name,
Component: 'CollectionFieldInitializer', Component: 'CollectionFieldInitializer',
find: findSchema,
remove: (schema, cb) => { remove: (schema, cb) => {
cb(schema, { cb(schema, {
breakRemoveOn: { breakRemoveOn: {
@ -172,23 +214,31 @@ export const useChartFilter = () => {
return resultItem; return resultItem;
}; };
const field2option = (field: any, depth: number, title: string, name: string): SchemaInitializerItemType => { const field2option = (
field: any,
depth: number,
title: string,
name: string,
fieldName: string,
): SchemaInitializerItemType => {
if (!field.interface) { if (!field.interface) {
return; return;
} }
const fieldInterface = getInterface(field.interface); const fieldInterface = fim.getFieldInterface(field.interface);
if (!fieldInterface?.filterable) { if (!fieldInterface?.filterable) {
return; return;
} }
const { nested, children } = fieldInterface.filterable; const { nested, children } = fieldInterface.filterable;
const fieldTitle = field.uiSchema?.title || field.name; const fieldTitle = field.uiSchema?.title || field.name;
const item = field2item(field, title, name); const item = field2item(field, title, name, fieldName);
if (field.target && depth > 2) { if (field.target && depth > 2) {
return; return;
} }
title = title ? `${title} / ${fieldTitle}` : fieldTitle; title = title ? `${title} / ${fieldTitle}` : fieldTitle;
if (children?.length && !['chinaRegion', 'createdBy', 'updatedBy'].includes(field.interface)) { if (children?.length && !['chinaRegion', 'createdBy', 'updatedBy', 'attachment'].includes(field.interface)) {
const items = children.map((child: any) => children2item(child, title, `${name}.${field.name}`)); const items = children.map((child: any) =>
children2item(child, title, `${name}.${field.name}`, `${fieldName}.${field.name}`),
);
return { return {
key: `${name}.${field.name}`, key: `${name}.${field.name}`,
name: field.name, name: field.name,
@ -201,9 +251,9 @@ export const useChartFilter = () => {
return item; return item;
} }
if (nested) { if (nested) {
const targetFields = getCollectionFields(field.target); const targetFields = cm.getCollectionFields(field.target);
const items = targetFields.map((targetField) => const items = targetFields.map((targetField) =>
field2option(targetField, depth + 1, '', `${name}.${field.name}`), field2option(targetField, depth + 1, '', `${name}.${field.name}`, `${fieldName}.${field.name}`),
); );
return { return {
key: `${name}.${field.name}`, key: `${name}.${field.name}`,
@ -220,12 +270,12 @@ export const useChartFilter = () => {
const associationOptions = []; const associationOptions = [];
fields.forEach((field) => { fields.forEach((field) => {
const fieldInterface = field.interface; const fieldInterface = field.interface;
const option = field2option(field, 0, '', collection.name); const option = field2option(field, 0, '', getFilterFieldPrefix(dataSource, collection.name), collection.name);
if (option) { if (option) {
options.push(option); options.push(option);
} }
if (['m2o'].includes(fieldInterface)) { if (['m2o'].includes(fieldInterface)) {
const option = field2option(field, 1, '', collection.name); const option = field2option(field, 1, '', getFilterFieldPrefix(dataSource, collection.name), collection.name);
if (option) { if (option) {
associationOptions.push(option); associationOptions.push(option);
} }
@ -251,40 +301,47 @@ export const useChartFilter = () => {
const getFilter = () => { const getFilter = () => {
const values = form?.values || {}; const values = form?.values || {};
const filter = {}; const filter = {};
Object.entries(fieldProps).forEach(([name, props]) => { Object.entries(fieldProps)
const { operator } = props || {}; .filter(([_, props]) => props)
const field = getCollectionJoinField(name); .forEach(([name, props]) => {
if (field?.target) { const { operator } = props || {};
name = `${name}.${field.targetKey || 'id'}`; const { dataSource, fieldName } = parseFilterFieldName(name);
} const ds = dm.getDataSource(dataSource);
const [collection, ...fields] = name.split('.'); const cm = ds.collectionManager;
const value = getValuesByPath(values, name); const field = cm.getCollectionField(fieldName);
const op = operator?.value || '$eq'; if (field?.target) {
if (collection !== 'custom') { name = `${fieldName}.${field.targetKey || 'id'}`;
filter[collection] = filter[collection] || { $and: [] }; }
const condition = {}; const [collection, ...fields] = fieldName.split('.');
lodash.set(condition, fields.join('.'), { [op]: value }); const value = getValuesByPath(values, name);
filter[collection].$and.push(condition); const op = operator?.value || '$eq';
} else { if (collection !== 'custom') {
filter[collection] = filter[collection] || {}; const key = getFilterFieldPrefix(dataSource, collection);
filter[collection][`$nFilter.${fields.join('.')}`] = value; filter[key] = filter[key] || { $and: [] };
} const condition = {};
}); lodash.set(condition, fields.join('.'), { [op]: value });
filter[key].$and.push(condition);
} else {
filter[collection] = filter[collection] || {};
filter[collection][`$nFilter.${fields.join('.')}`] = value;
}
});
return filter; return filter;
}; };
const hasFilter = (chart: { collection: string; query: any }, filterValues: any) => { const hasFilter = (chart: { dataSource: string; collection: string; query: any }, filterValues: any) => {
const { collection, query } = chart; const { dataSource, collection, query } = chart;
const { parameters } = parse(query.filter || ''); const { parameters } = parse(query.filter || '');
return ( return (
chart && chart &&
(filterValues[collection] || (filterValues[getFilterFieldPrefix(dataSource, collection)] ||
(filterValues['custom'] && parameters?.find((param: { key: string }) => filterValues['custom'][param.key]))) (filterValues['custom'] &&
parameters?.find(({ key }: { key: string }) => lodash.has(filterValues['custom'], key))))
); );
}; };
const appendFilter = (chart: { collection: string; query: any }, filterValues: any) => { const appendFilter = (chart: { dataSource: string; collection: string; query: any }, filterValues: any) => {
const { collection, query } = chart; const { dataSource, collection, query } = chart;
let newQuery = { ...query }; let newQuery = { ...query };
const originFilter = { ...(newQuery.filter || {}) }; const originFilter = { ...(newQuery.filter || {}) };
let filter = {}; let filter = {};
@ -297,7 +354,7 @@ export const useChartFilter = () => {
newQuery = { newQuery = {
...newQuery, ...newQuery,
filter: { filter: {
$and: [filter, filterValues[collection]], $and: [filter, filterValues[getFilterFieldPrefix(dataSource, collection)]],
}, },
}; };
return newQuery; return newQuery;
@ -308,8 +365,8 @@ export const useChartFilter = () => {
const requests = Object.values(charts) const requests = Object.values(charts)
.filter((chart) => hasFilter(chart, filterValues)) .filter((chart) => hasFilter(chart, filterValues))
.map((chart) => async () => { .map((chart) => async () => {
const { service, collection } = chart; const { dataSource, service, collection } = chart;
await service.runAsync(collection, appendFilter(chart, filterValues), true); await service.runAsync(dataSource, collection, appendFilter(chart, filterValues), true);
}); });
await Promise.all(requests.map((request) => request())); await Promise.all(requests.map((request) => request()));
}; };
@ -320,8 +377,8 @@ export const useChartFilter = () => {
return chart; return chart;
}) })
.map((chart) => async () => { .map((chart) => async () => {
const { service, collection, query } = chart; const { service, dataSource, collection, query } = chart;
await service.runAsync(collection, query, true); await service.runAsync(dataSource, collection, query, true);
}); });
await Promise.all(requests.map((request) => request())); await Promise.all(requests.map((request) => request()));
}; };
@ -375,14 +432,16 @@ export const useFilterVariable = () => {
export const useChartFilterSourceFields = () => { export const useChartFilterSourceFields = () => {
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
const { getChartCollections } = useChartData(); const { chartCollections } = useChartData();
const { getInterface, getCollectionFields, getCollection } = useCollectionManager_deprecated(); const dm = useDataSourceManager();
const fim = dm.collectionFieldInterfaceManager;
const { values } = useFieldComponents(); const { values } = useFieldComponents();
const field2option = (field: any, depth: number) => { const field2option = (cm: CollectionManager, field: any, depth: number) => {
if (!field.interface) { if (!field.interface) {
return; return;
} }
const fieldInterface = getInterface(field.interface); const fieldInterface = fim.getFieldInterface(field.interface);
if (!fieldInterface?.filterable) { if (!fieldInterface?.filterable) {
return; return;
} }
@ -398,8 +457,8 @@ export const useChartFilterSourceFields = () => {
return item; return item;
} }
if (nested) { if (nested) {
const targetFields = getCollectionFields(field.target); const targetFields = cm.getCollectionFields(field.target);
const items = targetFields.map((targetField) => field2option(targetField, depth + 1)); const items = targetFields.map((targetField) => field2option(cm, targetField, depth + 1));
return { return {
value: field.name, value: field.name,
label: t(field?.uiSchema?.title || field.name), label: t(field?.uiSchema?.title || field.name),
@ -412,29 +471,28 @@ export const useChartFilterSourceFields = () => {
return item; return item;
}; };
const collections = getChartCollections();
return useMemo(() => { return useMemo(() => {
const options = []; const options = Object.entries(chartCollections).map(([dataSource, collections]) => {
collections.forEach((name) => { const ds = dm.getDataSource(dataSource);
const collection = getCollection(name); return {
const children = []; value: dataSource,
const fields = getCollectionFields(collection); label: Schema.compile(ds.displayName, { t }),
fields.forEach((field) => { children: collections.map((name: string) => {
const option = field2option(field, 1); const cm = ds.collectionManager;
if (option) { const collection = cm.getCollection(name);
children.push(option); const fields = cm.getCollectionFields(name);
} const children = fields.map((field) => field2option(cm, field, 1)).filter((item) => item);
}); return {
if (children.length) { value: name,
options.push({ label: Schema.compile(collection.title, { t }),
value: name, children,
label: t(collection.title), };
children, }),
}); };
}
}); });
return options; return options;
}, [collections]); }, [chartCollections]);
}; };
export const useFieldComponents = () => { export const useFieldComponents = () => {
@ -457,26 +515,35 @@ export const useFieldComponents = () => {
}; };
}; };
export const useCollectionJoinFieldTitle = (name: string) => { export const useCollectionJoinFieldTitle = (dataSource: string, name: string) => {
const { getCollection, getCollectionField } = useCollectionManager_deprecated(); const { t } = useChartsTranslation();
const dm = useDataSourceManager();
const { showDataSource } = useChartData();
return useMemo(() => { return useMemo(() => {
const ds = dm.getDataSource(dataSource);
if (!ds) {
return;
}
const cm = ds.collectionManager;
if (!name) { if (!name) {
return; return;
} }
const [collectionName, ...fieldNames] = name.split('.'); const { fieldName } = parseFilterFieldName(name);
const [collectionName, ...fieldNames] = fieldName.split('.');
if (!fieldNames?.length) { if (!fieldNames?.length) {
return; return;
} }
const collection = getCollection(collectionName); const collection = cm.getCollection(collectionName);
let cName: any = collectionName; let cName: any = collectionName;
let field: any; let field: any;
let title = Schema.compile(collection?.title, { t: i18n.t }); let title = Schema.compile(collection?.title, { t });
while (cName && fieldNames.length > 0) { while (cName && fieldNames.length > 0) {
const fileName = fieldNames.shift(); const fileName = fieldNames.shift();
field = getCollectionField(`${cName}.${fileName}`); field = cm.getCollectionField(`${cName}.${fileName}`);
const fieldTitle = field?.uiSchema?.title || field?.name; const fieldTitle = field?.uiSchema?.title || field?.name;
if (fieldTitle) { if (fieldTitle) {
title += ` / ${Schema.compile(fieldTitle, { t: i18n.t })}`; title += ` / ${Schema.compile(fieldTitle, { t })}`;
} }
if (field?.target) { if (field?.target) {
cName = field.target; cName = field.target;
@ -484,6 +551,6 @@ export const useCollectionJoinFieldTitle = (name: string) => {
cName = null; cName = null;
} }
} }
return title; return showDataSource ? `${Schema.compile(ds.displayName, { t })} > ${title}` : title;
}, [name]); }, [name, dataSource, showDataSource]);
}; };

View File

@ -1,9 +1,13 @@
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { ISchema, Schema, useForm } from '@formily/react'; import { ISchema, Schema, useForm } from '@formily/react';
import { import {
CollectionFieldOptions,
CollectionFieldOptions_deprecated, CollectionFieldOptions_deprecated,
CollectionManager,
DEFAULT_DATA_SOURCE_KEY,
useACLRoleContext, useACLRoleContext,
useCollectionManager_deprecated, useCollectionManager_deprecated,
useDataSourceManager,
} from '@nocobase/client'; } from '@nocobase/client';
import { useContext, useMemo } from 'react'; import { useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -26,19 +30,24 @@ export type FieldOption = {
targetFields?: FieldOption[]; targetFields?: FieldOption[];
}; };
export const useChartDataSource = (dataSource?: string) => {
const { current } = useContext(ChartConfigContext);
const { dataSource: _dataSource = dataSource || DEFAULT_DATA_SOURCE_KEY, collection } = current || {};
const dm = useDataSourceManager();
const ds = dm.getDataSource(_dataSource);
const fim = dm.collectionFieldInterfaceManager;
const cm = ds?.collectionManager;
return { cm, fim, collection };
};
export const useFields = ( export const useFields = (
collection?: string, collectionFields: CollectionFieldOptions[],
): (CollectionFieldOptions_deprecated & { ): (CollectionFieldOptions & {
key: string; key: string;
label: string; label: string;
value: string; value: string;
})[] => { })[] => {
const { current } = useContext(ChartConfigContext); const fields = (collectionFields || [])
if (!collection) {
collection = current?.collection || '';
}
const { getCollectionFields } = useCollectionManager_deprecated();
const fields = (getCollectionFields(collection) || [])
.filter((field) => { .filter((field) => {
return field.interface; return field.interface;
}) })
@ -51,21 +60,22 @@ export const useFields = (
return fields; return fields;
}; };
export const useFieldsWithAssociation = (collection?: string) => { export const useFieldsWithAssociation = (dataSource?: string, collection?: string) => {
const { getCollectionFields, getInterface } = useCollectionManager_deprecated();
const { t } = useTranslation(); const { t } = useTranslation();
const fields = useFields(collection); const { cm, fim, collection: _collection } = useChartDataSource(dataSource);
const collectionFields = cm.getCollectionFields(collection || _collection);
const fields = useFields(collectionFields);
return useMemo( return useMemo(
() => () =>
fields.map((field) => { fields.map((field) => {
const filterable = getInterface(field.interface)?.filterable; const filterable = fim.getFieldInterface(field.interface)?.filterable;
const label = Schema.compile(field.uiSchema?.title || field.name, { t }); const label = Schema.compile(field.uiSchema?.title || field.name, { t });
if (!(filterable && (filterable?.nested || filterable?.children?.length))) { if (!(filterable && (filterable?.nested || filterable?.children?.length))) {
return { ...field, label }; return { ...field, label };
} }
let targetFields = []; let targetFields = [];
if (filterable?.nested) { if (filterable?.nested) {
const nestedFields = (getCollectionFields(field.target) || []) const nestedFields = (cm.getCollectionFields(field.target) || [])
.filter((targetField) => { .filter((targetField) => {
return targetField.interface; return targetField.interface;
}) })
@ -132,20 +142,23 @@ export const useFormatters = (fields: FieldOption[]) => (field: any) => {
export const useCollectionOptions = () => { export const useCollectionOptions = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { collections } = useCollectionManager_deprecated(); const dm = useDataSourceManager();
const { allowAll, parseAction } = useACLRoleContext(); const { parseAction } = useACLRoleContext();
const options = collections const allCollections = dm.getAllCollections({
.filter((collection: { name: string }) => { filterCollection: (collection) => {
if (allowAll) {
return true;
}
const params = parseAction(`${collection.name}:list`); const params = parseAction(`${collection.name}:list`);
return params; return params;
}) },
.map((collection: { name: string; title: string }) => ({ });
label: collection.title, const options = allCollections
value: collection.name, .filter(({ key, isDBInstance }) => key === DEFAULT_DATA_SOURCE_KEY || isDBInstance)
key: collection.name, .map(({ key, displayName, collections }) => ({
value: key,
label: displayName,
children: collections.map((collection) => ({
value: collection.name,
label: collection.title,
})),
})); }));
return useMemo(() => Schema.compile(options, { t }), [options]); return useMemo(() => Schema.compile(options, { t }), [options]);
}; };
@ -202,11 +215,124 @@ export const useOrderReaction = (defaultOptions: any[], fields: FieldOption[]) =
field.setValue(newOrders); field.setValue(newOrders);
}; };
export const useData = (data?: any[], collection?: string) => { export const useData = (data?: any[], dataSource?: string, collection?: string) => {
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
const { service, query } = useContext(ChartRendererContext); const { service, query } = useContext(ChartRendererContext);
const fields = useFieldsWithAssociation(collection); const fields = useFieldsWithAssociation(dataSource, collection);
const form = useForm(); const form = useForm();
const selectedFields = getSelectedFields(fields, form?.values?.query || query); const selectedFields = getSelectedFields(fields, form?.values?.query || query);
return processData(selectedFields, service?.data || data || [], { t }); return processData(selectedFields, service?.data || data || [], { t });
}; };
export const useCollectionFieldsOptions = (dataSource: string, collectionName: string, maxDepth = 2, excludes = []) => {
const { cm, fim, collection } = useChartDataSource(dataSource);
const collectionFields = cm.getCollectionFields(collectionName || collection);
const fields = collectionFields.filter((v) => !excludes.includes(v.interface));
const field2option = (field, depth, prefix?) => {
if (!field.interface) {
return;
}
const fieldInterface = fim.getFieldInterface(field.interface);
if (!fieldInterface?.filterable) {
return;
}
const { nested, children } = fieldInterface.filterable;
const value = prefix ? `${prefix}.${field.name}` : field.name;
const option = {
...field,
name: field.name,
title: field?.uiSchema?.title || field.name,
schema: field?.uiSchema,
key: value,
};
if (field.target && depth > maxDepth) {
return;
}
if (depth > maxDepth) {
return option;
}
if (children?.length) {
option['children'] = children.map((v) => {
return {
...v,
key: `${field.name}.${v.name}`,
};
});
}
if (nested) {
const targetFields = cm.getCollectionFields(field.target).filter((v) => !excludes.includes(v.interface));
const options = getOptions(targetFields, depth + 1, field.name).filter(Boolean);
option['children'] = option['children'] || [];
option['children'].push(...options);
}
return option;
};
const getOptions = (fields, depth, prefix?) => {
const options = [];
fields.forEach((field) => {
const option = field2option(field, depth, prefix);
if (option) {
options.push(option);
}
});
return options;
};
const options = getOptions(fields, 1);
return options;
};
export const useCollectionFilterOptions = (dataSource: string, collection: string) => {
const { cm, fim, collection: _collection } = useChartDataSource(dataSource);
return useMemo(() => {
const fields = cm.getCollectionFields(collection || _collection);
const field2option = (field, depth) => {
if (!field.interface) {
return;
}
const fieldInterface = fim.getFieldInterface(field.interface);
if (!fieldInterface?.filterable) {
return;
}
const { nested, children, operators } = fieldInterface.filterable;
const option = {
name: field.name,
title: field?.uiSchema?.title || field.name,
schema: field?.uiSchema,
operators:
operators?.filter?.((operator) => {
return !operator?.visible || operator.visible(field);
}) || [],
interface: field.interface,
};
if (field.target && depth > 2) {
return;
}
if (depth > 2) {
return option;
}
if (children?.length) {
option['children'] = children;
}
if (nested) {
const targetFields = cm.getCollectionFields(field.target);
const options = getOptions(targetFields, depth + 1).filter(Boolean);
option['children'] = option['children'] || [];
option['children'].push(...options);
}
return option;
};
const getOptions = (fields, depth) => {
const options = [];
fields.forEach((field) => {
const option = field2option(field, depth);
if (option) {
options.push(option);
}
});
return options;
};
const options = getOptions(fields, 1);
return options;
}, [collection, cm, fim]);
};

View File

@ -8,6 +8,7 @@ import {
gridRowColWrap, gridRowColWrap,
useAPIClient, useAPIClient,
useCollection_deprecated, useCollection_deprecated,
useDataSource,
useDesignable, useDesignable,
} from '@nocobase/client'; } from '@nocobase/client';
import { Empty, Result, Spin, Typography } from 'antd'; import { Empty, Result, Spin, Typography } from 'antd';
@ -27,9 +28,9 @@ export const ChartRenderer: React.FC & {
} = (props) => { } = (props) => {
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
const ctx = useContext(ChartRendererContext); const ctx = useContext(ChartRendererContext);
const { config, transform, collection, service, data: _data } = ctx; const { config, transform, dataSource, collection, service, data: _data } = ctx;
const fields = useFieldsWithAssociation(collection); const fields = useFieldsWithAssociation(dataSource, collection);
const data = useData(_data, collection); const data = useData(_data, dataSource, collection);
const general = config?.general || {}; const general = config?.general || {};
const advanced = config?.advanced || {}; const advanced = config?.advanced || {};
const api = useAPIClient(); const api = useAPIClient();
@ -70,7 +71,7 @@ export const ChartRenderer: React.FC & {
} }
if (!(data && data.length)) { if (!(data && data.length)) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('Please configure and run query')} />; return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('No data')} />;
} }
return <C />; return <C />;
@ -84,14 +85,15 @@ ChartRenderer.Designer = function Designer() {
const field = useField(); const field = useField();
const schema = useFieldSchema(); const schema = useFieldSchema();
const { insertAdjacent } = useDesignable(); const { insertAdjacent } = useDesignable();
const { name, title, dataSource } = useCollection_deprecated(); const dataSource = useDataSource();
const { name, title } = useCollection_deprecated();
return ( return (
<GeneralSchemaDesigner disableInitializer title={title || name}> <GeneralSchemaDesigner disableInitializer title={title || name}>
<SchemaSettingsItem <SchemaSettingsItem
title="Configure" title="Configure"
key="configure" key="configure"
onClick={async () => { onClick={async () => {
setCurrent({ schema, field, dataSource, collection: name, service, data: service.data }); setCurrent({ schema, field, dataSource: dataSource.key, collection: name, service, data: service.data });
setVisible(true); setVisible(true);
}} }}
> >

View File

@ -1,6 +1,7 @@
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { import {
CollectionManagerProvider, CollectionManagerProvider,
DEFAULT_DATA_SOURCE_KEY,
MaybeCollectionProvider, MaybeCollectionProvider,
useAPIClient, useAPIClient,
useDataSourceManager, useDataSourceManager,
@ -68,14 +69,14 @@ export const ChartRendererContext = createContext<
ChartRendererContext.displayName = 'ChartRendererContext'; ChartRendererContext.displayName = 'ChartRendererContext';
export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => { export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
const { query, config, collection, transform, dataSource } = props; const { query, config, collection, transform, dataSource = DEFAULT_DATA_SOURCE_KEY } = props;
const { addChart } = useContext(ChartDataContext); const { addChart } = useContext(ChartDataContext);
const { ready, form, enabled } = useContext(ChartFilterContext); const { ready, form, enabled } = useContext(ChartFilterContext);
const { getFilter, hasFilter, appendFilter } = useChartFilter(); const { getFilter, hasFilter, appendFilter } = useChartFilter();
const schema = useFieldSchema(); const schema = useFieldSchema();
const api = useAPIClient(); const api = useAPIClient();
const service = useRequest( const service = useRequest(
(collection, query, manual) => (dataSource, collection, query, manual) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
// Check if the chart is configured // Check if the chart is configured
if (!(collection && query?.measures?.length)) return resolve(undefined); if (!(collection && query?.measures?.length)) return resolve(undefined);
@ -83,8 +84,8 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
if (enabled && !form) return resolve(undefined); if (enabled && !form) return resolve(undefined);
const filterValues = getFilter(); const filterValues = getFilter();
const queryWithFilter = const queryWithFilter =
!manual && hasFilter({ collection, query }, filterValues) !manual && hasFilter({ dataSource, collection, query }, filterValues)
? appendFilter({ collection, query }, filterValues) ? appendFilter({ dataSource, collection, query }, filterValues)
: query; : query;
api api
.request({ .request({
@ -92,6 +93,7 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
method: 'POST', method: 'POST',
data: { data: {
uid: schema?.['x-uid'], uid: schema?.['x-uid'],
dataSource,
collection, collection,
...queryWithFilter, ...queryWithFilter,
filter: removeUnparsableFilter(queryWithFilter.filter), filter: removeUnparsableFilter(queryWithFilter.filter),
@ -115,14 +117,16 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
}) })
.then((res) => { .then((res) => {
resolve(res?.data?.data); resolve(res?.data?.data);
})
.finally(() => {
if (!manual && schema?.['x-uid']) { if (!manual && schema?.['x-uid']) {
addChart(schema?.['x-uid'], { collection, service, query }); addChart(schema?.['x-uid'], { dataSource, collection, service, query });
} }
}) })
.catch(reject); .catch(reject);
}), }),
{ {
defaultParams: [collection, query], defaultParams: [dataSource, collection, query],
// Wait until ChartFilterProvider is rendered and check the status of the filter form // Wait until ChartFilterProvider is rendered and check the status of the filter form
// since the filter parameters should be applied if the filter block is enabled // since the filter parameters should be applied if the filter block is enabled
ready: ready && (!enabled || !!form), ready: ready && (!enabled || !!form),
@ -131,9 +135,9 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
return ( return (
<CollectionManagerProvider dataSource={dataSource}> <CollectionManagerProvider dataSource={dataSource}>
<MaybeCollectionProvider collection={collection} dataSource={dataSource}> <MaybeCollectionProvider collection={collection}>
<ConfigProvider card={{ style: { boxShadow: 'none' } }}> <ConfigProvider card={{ style: { boxShadow: 'none' } }}>
<ChartRendererContext.Provider value={{ collection, config, transform, service, query }}> <ChartRendererContext.Provider value={{ dataSource, collection, config, transform, service, query }}>
{props.children} {props.children}
</ChartRendererContext.Provider> </ChartRendererContext.Provider>
</ConfigProvider> </ConfigProvider>

View File

@ -51,6 +51,7 @@ describe('api', () => {
test('query', async () => { test('query', async () => {
const ctx = { const ctx = {
app,
db, db,
action: { action: {
params: { params: {
@ -82,6 +83,7 @@ describe('api', () => {
test('query with sort', async () => { test('query with sort', async () => {
const ctx = { const ctx = {
app,
db, db,
action: { action: {
params: { params: {

View File

@ -1,8 +1,9 @@
import { MockServer, mockServer } from '@nocobase/test'; import { MockServer, createMockServer } from '@nocobase/test';
import compose from 'koa-compose'; import compose from 'koa-compose';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { cacheMiddleware, parseBuilder, parseFieldAndAssociations } from '../actions/query'; import { cacheMiddleware, parseBuilder, parseFieldAndAssociations } from '../actions/query';
const formatter = await import('../actions/formatter'); const formatter = await import('../actions/formatter');
describe('query', () => { describe('query', () => {
describe('parseBuilder', () => { describe('parseBuilder', () => {
const sequelize = { const sequelize = {
@ -11,8 +12,10 @@ describe('query', () => {
}; };
let ctx: any; let ctx: any;
let app: MockServer; let app: MockServer;
beforeAll(() => { beforeAll(async () => {
app = mockServer(); app = await createMockServer({
plugins: ['data-source-manager'],
});
app.db.options.underscored = true; app.db.options.underscored = true;
app.db.collection({ app.db.collection({
name: 'orders', name: 'orders',

View File

@ -27,6 +27,7 @@ type OrderProps = {
type QueryParams = Partial<{ type QueryParams = Partial<{
uid: string; uid: string;
dataSource: string;
collection: string; collection: string;
measures: MeasureProps[]; measures: MeasureProps[];
dimensions: DimensionProps[]; dimensions: DimensionProps[];
@ -45,6 +46,11 @@ type QueryParams = Partial<{
refresh: boolean; refresh: boolean;
}>; }>;
const getDB = (ctx: Context, dataSource: string) => {
const ds = ctx.app.dataSourceManager.dataSources.get(dataSource);
return ds?.collectionManager.db;
};
export const postProcess = async (ctx: Context, next: Next) => { export const postProcess = async (ctx: Context, next: Next) => {
const { data, fieldMap } = ctx.action.params.values as { const { data, fieldMap } = ctx.action.params.values as {
data: any[]; data: any[];
@ -71,8 +77,9 @@ export const postProcess = async (ctx: Context, next: Next) => {
}; };
export const queryData = async (ctx: Context, next: Next) => { export const queryData = async (ctx: Context, next: Next) => {
const { collection, queryParams, fieldMap } = ctx.action.params.values; const { dataSource, collection, queryParams, fieldMap } = ctx.action.params.values;
const model = ctx.db.getModel(collection); const db = getDB(ctx, dataSource) || ctx.db;
const model = db.getModel(collection);
const data = await model.findAll(queryParams); const data = await model.findAll(queryParams);
ctx.action.params.values = { ctx.action.params.values = {
data, data,
@ -89,8 +96,9 @@ export const queryData = async (ctx: Context, next: Next) => {
}; };
export const parseBuilder = async (ctx: Context, next: Next) => { export const parseBuilder = async (ctx: Context, next: Next) => {
const { sequelize } = ctx.db; const { dataSource, measures, dimensions, orders, include, where, limit } = ctx.action.params.values;
const { measures, dimensions, orders, include, where, limit } = ctx.action.params.values; const db = getDB(ctx, dataSource) || ctx.db;
const { sequelize } = db;
const attributes = []; const attributes = [];
const group = []; const group = [];
const order = []; const order = [];
@ -157,8 +165,16 @@ export const parseBuilder = async (ctx: Context, next: Next) => {
}; };
export const parseFieldAndAssociations = async (ctx: Context, next: Next) => { export const parseFieldAndAssociations = async (ctx: Context, next: Next) => {
const { collection: collectionName, measures, dimensions, orders, filter } = ctx.action.params.values as QueryParams; const {
const collection = ctx.db.getCollection(collectionName); dataSource,
collection: collectionName,
measures,
dimensions,
orders,
filter,
} = ctx.action.params.values as QueryParams;
const db = getDB(ctx, dataSource) || ctx.db;
const collection = db.getCollection(collectionName);
const fields = collection.fields; const fields = collection.fields;
const models: { const models: {
[target: string]: { [target: string]: {
@ -180,7 +196,7 @@ export const parseFieldAndAssociations = async (ctx: Context, next: Next) => {
let fieldType = fields.get(name)?.type; let fieldType = fields.get(name)?.type;
if (target) { if (target) {
const targetField = fields.get(target) as Field; const targetField = fields.get(target) as Field;
const targetCollection = ctx.db.getCollection(targetField.target); const targetCollection = db.getCollection(targetField.target);
const targetFields = targetCollection.fields; const targetFields = targetCollection.fields;
fieldType = targetFields.get(name)?.type; fieldType = targetFields.get(name)?.type;
field = `${target}.${field}`; field = `${target}.${field}`;