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[];
errorMessage?: string;
status?: 'loaded' | 'loading-failed' | 'loading' | 'reloading';
isDBInstance?: boolean;
}
export type DataSourceFactory = new (options: DataSourceOptions, dataSourceManager: DataSourceManager) => DataSource;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,11 +35,11 @@ export class G2PlotChart extends Chart {
},
},
tooltip: (d, index: number, data, column: any) => {
const field = column.y.field;
const field = column.y?.field;
const props = fieldProps[field];
const name = props?.label || field;
const transformer = props?.transformer;
const value = column.y.value[index];
const value = column.y?.value[index];
return { name, value: transformer ? transformer(value) : value };
},
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 { Form as FormType, ObjectField, createForm, onFieldChange, onFormInit } from '@formily/core';
import { FormConsumer, ISchema, Schema } from '@formily/react';
import {
AutoComplete,
FormProvider,
SchemaComponent,
gridRowColWrap,
useCollectionFieldsOptions,
useCollectionFilterOptions,
useDesignable,
} from '@nocobase/client';
import { AutoComplete, FormProvider, SchemaComponent, gridRowColWrap, useDesignable } from '@nocobase/client';
import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography } from 'antd';
import { cloneDeep, isEqual } from 'lodash';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import {
useChartFields,
useCollectionOptions,
useCollectionFieldsOptions,
useCollectionFilterOptions,
useData,
useFieldTypes,
useFieldsWithAssociation,
@ -57,13 +51,13 @@ export const ChartConfigure: React.FC<{
const { t } = useChartsTranslation();
const { service } = useContext(ChartRendererContext);
const { visible, setVisible, current } = useContext(ChartConfigContext);
const { schema, field, collection, initialValues } = current || {};
const { schema, field, dataSource, collection, initialValues } = current || {};
const { dn } = useDesignable();
const { modal } = App.useApp();
const { insert } = props;
const charts = useCharts();
const fields = useFieldsWithAssociation(collection);
const fields = useFieldsWithAssociation(dataSource, collection);
const initChart = (overwrite = false) => {
if (!form.modified) {
return;
@ -108,15 +102,19 @@ export const ChartConfigure: React.FC<{
const form = useMemo(
() =>
createForm({
values: { config: { chartType }, ...(initialValues || field?.decoratorProps), collection },
values: {
config: { chartType },
...(initialValues || field?.decoratorProps),
collection: [dataSource, collection],
},
effects: (form) => {
onFieldChange('config.chartType', () => initChart(true));
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
[field, visible, collection],
[field, visible, dataSource, collection],
);
const RunButton: React.FC = () => (
@ -133,7 +131,7 @@ export const ChartConfigure: React.FC<{
}
try {
await service.runAsync(collection, form.values.query, true);
await service.runAsync(dataSource, collection, form.values.query, true);
} catch (e) {
console.log(e);
}
@ -163,7 +161,7 @@ export const ChartConfigure: React.FC<{
const { query, config, transform, mode } = form.values;
const afterSave = () => {
setVisible(false);
current.service?.run(collection, query);
current.service?.run(dataSource, collection, query);
queryRef.current.scrollTop = 0;
configRef.current.scrollTop = 0;
service.mutate(undefined);
@ -171,6 +169,7 @@ export const ChartConfigure: React.FC<{
const rendererProps = {
query,
config,
dataSource,
collection,
transform,
mode: mode || 'builder',
@ -308,19 +307,20 @@ ChartConfigure.Query = function Query() {
const useFormatterOptions = useFormatters(fields);
const collectionOptions = useCollectionOptions();
const { current, setCurrent } = useContext(ChartConfigContext);
const { collection } = current || {};
const fieldOptions = useCollectionFieldsOptions(collection, 1);
const { dataSource, collection } = current || {};
const fieldOptions = useCollectionFieldsOptions(dataSource, collection, 1);
const compiledFieldOptions = Schema.compile(fieldOptions, { t });
const filterOptions = useCollectionFilterOptions(collection);
const filterOptions = useCollectionFilterOptions(dataSource, collection);
const { service } = useContext(ChartRendererContext);
const onCollectionChange = (value: string) => {
const onCollectionChange = (value: string[]) => {
const { schema, field } = current;
const [dataSource, collection] = value;
setCurrent({
schema,
field,
collection: value,
dataSource: current.dataSource,
collection,
dataSource,
service: current.service,
initialValues: {},
data: undefined,

View File

@ -150,9 +150,9 @@ export const querySchema: ISchema = {
title: '{{t("Collection")}}',
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component': 'Cascader',
enum: '{{ collectionOptions }}',
'x-component-props': {
options: '{{ collectionOptions }}',
onChange: '{{ onCollectionChange }}',
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 = () => {
const { t } = useChartsTranslation();
return (
<GeneralSchemaDesigner disableInitializer title={t('Filter')}>
<GeneralSchemaDesigner disableInitializer title={t('Filter')} showDataSource={false}>
{/* <SchemaSettings.BlockTitleItem /> */}
{/* <SchemaSettings.Divider /> */}
<SchemaSettingsRemove

View File

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

View File

@ -1,7 +1,7 @@
import React, { memo, useContext, useEffect, useMemo, useRef } from 'react';
import { createForm, onFieldInit, onFieldMount, onFieldUnmount } from '@formily/core';
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 { useChartFilter } from '../hooks';
@ -33,7 +33,10 @@ export const ChartFilterForm: React.FC = memo((props) => {
if (!name) {
return;
}
setField(name, { title: field.title, operator: field.componentProps['filter-operator'] });
setField(name, {
title: field.title,
operator: field.componentProps['filter-operator'],
});
// parse field title
if (field.title.includes('/')) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,13 @@
import { ArrayField } from '@formily/core';
import { ISchema, Schema, useForm } from '@formily/react';
import {
CollectionFieldOptions,
CollectionFieldOptions_deprecated,
CollectionManager,
DEFAULT_DATA_SOURCE_KEY,
useACLRoleContext,
useCollectionManager_deprecated,
useDataSourceManager,
} from '@nocobase/client';
import { useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@ -26,19 +30,24 @@ export type 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 = (
collection?: string,
): (CollectionFieldOptions_deprecated & {
collectionFields: CollectionFieldOptions[],
): (CollectionFieldOptions & {
key: string;
label: string;
value: string;
})[] => {
const { current } = useContext(ChartConfigContext);
if (!collection) {
collection = current?.collection || '';
}
const { getCollectionFields } = useCollectionManager_deprecated();
const fields = (getCollectionFields(collection) || [])
const fields = (collectionFields || [])
.filter((field) => {
return field.interface;
})
@ -51,21 +60,22 @@ export const useFields = (
return fields;
};
export const useFieldsWithAssociation = (collection?: string) => {
const { getCollectionFields, getInterface } = useCollectionManager_deprecated();
export const useFieldsWithAssociation = (dataSource?: string, collection?: string) => {
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(
() =>
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 });
if (!(filterable && (filterable?.nested || filterable?.children?.length))) {
return { ...field, label };
}
let targetFields = [];
if (filterable?.nested) {
const nestedFields = (getCollectionFields(field.target) || [])
const nestedFields = (cm.getCollectionFields(field.target) || [])
.filter((targetField) => {
return targetField.interface;
})
@ -132,20 +142,23 @@ export const useFormatters = (fields: FieldOption[]) => (field: any) => {
export const useCollectionOptions = () => {
const { t } = useTranslation();
const { collections } = useCollectionManager_deprecated();
const { allowAll, parseAction } = useACLRoleContext();
const options = collections
.filter((collection: { name: string }) => {
if (allowAll) {
return true;
}
const dm = useDataSourceManager();
const { parseAction } = useACLRoleContext();
const allCollections = dm.getAllCollections({
filterCollection: (collection) => {
const params = parseAction(`${collection.name}:list`);
return params;
})
.map((collection: { name: string; title: string }) => ({
label: collection.title,
value: collection.name,
key: collection.name,
},
});
const options = allCollections
.filter(({ key, isDBInstance }) => key === DEFAULT_DATA_SOURCE_KEY || isDBInstance)
.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]);
};
@ -202,11 +215,124 @@ export const useOrderReaction = (defaultOptions: any[], fields: FieldOption[]) =
field.setValue(newOrders);
};
export const useData = (data?: any[], collection?: string) => {
export const useData = (data?: any[], dataSource?: string, collection?: string) => {
const { t } = useChartsTranslation();
const { service, query } = useContext(ChartRendererContext);
const fields = useFieldsWithAssociation(collection);
const fields = useFieldsWithAssociation(dataSource, collection);
const form = useForm();
const selectedFields = getSelectedFields(fields, form?.values?.query || query);
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,
useAPIClient,
useCollection_deprecated,
useDataSource,
useDesignable,
} from '@nocobase/client';
import { Empty, Result, Spin, Typography } from 'antd';
@ -27,9 +28,9 @@ export const ChartRenderer: React.FC & {
} = (props) => {
const { t } = useChartsTranslation();
const ctx = useContext(ChartRendererContext);
const { config, transform, collection, service, data: _data } = ctx;
const fields = useFieldsWithAssociation(collection);
const data = useData(_data, collection);
const { config, transform, dataSource, collection, service, data: _data } = ctx;
const fields = useFieldsWithAssociation(dataSource, collection);
const data = useData(_data, dataSource, collection);
const general = config?.general || {};
const advanced = config?.advanced || {};
const api = useAPIClient();
@ -70,7 +71,7 @@ export const ChartRenderer: React.FC & {
}
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 />;
@ -84,14 +85,15 @@ ChartRenderer.Designer = function Designer() {
const field = useField();
const schema = useFieldSchema();
const { insertAdjacent } = useDesignable();
const { name, title, dataSource } = useCollection_deprecated();
const dataSource = useDataSource();
const { name, title } = useCollection_deprecated();
return (
<GeneralSchemaDesigner disableInitializer title={title || name}>
<SchemaSettingsItem
title="Configure"
key="configure"
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);
}}
>

View File

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

View File

@ -51,6 +51,7 @@ describe('api', () => {
test('query', async () => {
const ctx = {
app,
db,
action: {
params: {
@ -82,6 +83,7 @@ describe('api', () => {
test('query with sort', async () => {
const ctx = {
app,
db,
action: {
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 { vi } from 'vitest';
import { cacheMiddleware, parseBuilder, parseFieldAndAssociations } from '../actions/query';
const formatter = await import('../actions/formatter');
describe('query', () => {
describe('parseBuilder', () => {
const sequelize = {
@ -11,8 +12,10 @@ describe('query', () => {
};
let ctx: any;
let app: MockServer;
beforeAll(() => {
app = mockServer();
beforeAll(async () => {
app = await createMockServer({
plugins: ['data-source-manager'],
});
app.db.options.underscored = true;
app.db.collection({
name: 'orders',

View File

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