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