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:
parent
d691e4c7e6
commit
088f3977a2
@ -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;
|
||||
|
@ -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'}>
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
@ -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 }) => {
|
||||
|
@ -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 }));
|
||||
|
@ -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: {
|
||||
|
@ -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: {
|
||||
|
@ -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,
|
||||
|
@ -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")}}',
|
||||
},
|
||||
|
@ -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'} />;
|
||||
};
|
@ -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
|
||||
|
@ -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 }}
|
||||
>
|
||||
|
@ -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('/')) {
|
||||
|
@ -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 && (
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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]: {
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
@ -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]);
|
||||
};
|
||||
|
@ -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]);
|
||||
};
|
||||
|
@ -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);
|
||||
}}
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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: {
|
||||
|
@ -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',
|
||||
|
@ -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}`;
|
||||
|
Loading…
Reference in New Issue
Block a user