feat(bi): filter block for charts (#2851)

* refactor:  SchemaInitializer

* feat: render

* feat: finish BlockInitializers

* feat: finish TableActionInitializers

* refactor: new SchemaInitializerV2

* feat: support useChildren

* feat: finish TableColumnInitializers

* refactor: tableActionColumnInitializers

* fix: bug

* refactor: xx

* docs: schemaInitializer demo1

* docs: schema-initializer demo2

* feat: demo3

* docs: schema-initializer

* fix: form

* feat(bi): filter block for charts

* refactor: schema-initializer

* fix: bug

* feat: add filter block in chart block

* refactor: schema initializer

* refactor: rename

* fix: delete SchemaInitializerProvider

* refactor: props `insert` to hooks `useSchemaInitializerV2`

* fix: bug

* refactor: improve code & style

* feat: add expand/collapse button

* fix: filter & reset bug

* fix: configure data conflict

* refactor: delete `SchemaInitializer.Button`

* refactor: delete old SchemaInitializer

* feat: support for adding custom filter field

* fix: bug

* fix: workflow

* fix: docs

* feat: support for editing operator of collection field

* fix: bug

* fix: bug

* feat: support for editing options of custom fields

* feat: style

* fix: remove v2

* feat: init chart with filter

* fix: visible

* fix: bug

* fix: item hook

* feat: support for setting default value

* feat: item hook

* feat: support for filtering by associate fields

* fix: bug of associate field

* fix: get field bug

* fix: bug of adding filter block

* fix: bug of empty filter

* fix: filter parse issues

* fix: add search DataBlockInitializer

* fix: build bug

* fix: fix bug and improve

* fix: bug

* fix: variable

* fix: hook dependencies

* fix: style bug

* fix: style bug

* fix: test bug

* fix: test bug

* fix: rerender bug

* fix: remove menu select

* fix: bug

* chore: add aria-label for SchemaInitializerButton

* refactor: rename name to camel case

* fix: menu height bug

* fix: build errors

* fix: build errors

* fix: bug

* fix: bug

* fix: performance

* fix: fix T-2388, T-2384

* test: add test for header

* fix: sidebar is not refresh (T-2422)

* feat(e2e): support to add group page and link page

* chore: make sure the page is configurable when using page.goto

* test: add tests for menu initializer

* fix: imporve  code

* fix: fix T-2383

* fix: variables

* fix: variables

* fix: fix T-2441

* chore: fix build error

* chore: optimize locator of menu item

* refactor: rename testid for select

* feat: select source field for custom field, fix T-2416

* fix: bug

* test: make tests passing

* fix: make tests passing

* chore: upgrade vitest to v0.34.6

* chore: increase timeout of e2e

* feat: core

* fix: revert schema initializer demos

* test: menu, page tabs, page grid, table column

* fix: schema button interface

* fix: fix T-2386

* feat: refactor: page tab settings

* feat: page settings

* fix: dumirc

* fix: export CSSVariableProvider

* feat: lazy render

* fix: fix T-2473, T-2474

* fix: fix T-2476

* fix: fix T-2472

* fix: form-item

* fix: general schema desinger

* feat: filter form item settings

* fix: fix T-2484, T-2482

* refactor: form-v2 schema settings

* refactor: form-v1 schema settings

* refactor: action schema settings

* fix: dependency

* fix: action bug

* fix: form-item bug

* fix: types error

* docs: schema settings  doc

* docs: schema settings

* feat: schema setting  item add name

* fix: visible lazy render bug

* fix: revert form item filter

* fix: test bug

* fix: test JSON.parse bug

* fix: test bug

* fix: improve styling

* fix: styling

* fix: cleanup

* fix: token.borderRadiusSM

* fix: bug

* test: add tests

* fix: style bug

* fix: add chart performance

* feat: add SchemaDesignerContext

* fix: bug

* fix: test bug

* style: create record action style improve

* fix: make test passing

* chore: mack tests passing

* chore: make tests passing

* test: fix tests

* style: style revert

* fix: bug

* fix: data selector

* fix: fix tests

* fix: fix tests

* fix: delete PluginManagerContext

* refactor: improve router and add SchemaComponentProvider & CSSVariableProvider to MainComponent

* fix: add dn and field builtin to SchemaSettingWrapper

* feat: update docs

* refactor: application providers

* fix: test bug

* fix: fix tests

* chore: make test passing

* feat: update docs

* chore: rename collection name

* feat: update docs

* chore: skip weird test

* fix: blockInitializers media to otherBlocks

* fix: cancel to skip test

* fix: bug

* test: add test

* refactor: migrate to small files

* test: add tests for form block settings

* chore: format

* fix: add chart scroll bug

* refactor: action designer improve

* refactor: formitem designer schemaSetting

* feat: schemaSettingsManager and schemaInitializerManager addItem and removeItem

* test: add tests for color field in creating block

* test: add tests for email field in creating block

* test: make tests passing

* perf: reduce fields number

* fix: sub menu bug

* chore: update filter hook

* chore: fix association fields

* fix: build

* chore: use `default` instead of `componentProps.defaultValue`

* chore: remove chinaRegion in custom field components

* fix: menu height

* fix: duplicate keys warning

* fix: variable issues

* fix: filter bug

* fix: operator bug

---------

Co-authored-by: Your Name <1098626505@qq.com>
Co-authored-by: Rain <958414905@qq.com>
Co-authored-by: chenos <chenlinxh@gmail.com>
Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
YANG QIA 2023-12-05 20:45:18 +08:00 committed by GitHub
parent 536001eaba
commit 16ad19b18d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2110 additions and 137 deletions

View File

@ -46,3 +46,4 @@ export * from './schema-templates';
export * from './style';
export * from './system-settings';
export * from './user';
export * from './variables';

View File

@ -48,7 +48,7 @@ export function useAssociationFieldContext<F extends GeneralField>() {
}
export default function useServiceOptions(props) {
const { action = 'list', service } = props;
const { action = 'list', service, useOriginalFilter } = props;
const fieldSchema = useFieldSchema();
const field = useField();
const { getField } = useCollection();
@ -119,7 +119,7 @@ export default function useServiceOptions(props) {
return mergeFilter(
[
mergeFilter([
isOToAny && !isInFilterFormBlock(fieldSchema) && collectionField?.foreignKey
isOToAny && !isInFilterFormBlock(fieldSchema) && collectionField?.foreignKey && !useOriginalFilter
? {
[collectionField.foreignKey]: {
$is: null,
@ -132,7 +132,8 @@ export default function useServiceOptions(props) {
sourceValue !== undefined &&
sourceValue !== null &&
!isInFilterFormBlock(fieldSchema) &&
collectionField?.foreignKey
collectionField?.foreignKey &&
!useOriginalFilter
? {
[collectionField.foreignKey]: {
$eq: sourceValue,
@ -149,7 +150,14 @@ export default function useServiceOptions(props) {
],
'$or',
);
}, [collectionField?.interface, collectionField?.foreignKey, fieldSchema, fieldServiceFilter, sourceValue]);
}, [
collectionField?.interface,
collectionField?.foreignKey,
fieldSchema,
fieldServiceFilter,
sourceValue,
useOriginalFilter,
]);
return useMemo(() => {
return {

View File

@ -1,3 +1,4 @@
export * from './FormItem';
export * from './FormItem.Settings';
export * from './FormItem.FilterFormSettings';
export * from './SchemaSettingOptions';

View File

@ -2,3 +2,4 @@ export * from './Input';
export * from './ReadPretty';
export * from './Json';
export * from './EllipsisWithTooltip';
export * from './shared';

View File

@ -46,7 +46,7 @@ const InternalRemoteSelect = connect(
const { getField } = useCollection();
const searchData = useRef(null);
const { getCollectionJoinField, getInterface } = useCollectionManager();
const collectionField = getField(fieldSchema.name);
const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema.name as string);
const targetField =
_targetField ||
(collectionField?.target &&

View File

@ -0,0 +1,3 @@
export * from './useBaseVariable';
export * from './useDateVariable';
export * from './useUserVariable';

View File

@ -0,0 +1,2 @@
export * from './hooks';
export * from './VariableInput';

View File

@ -3,3 +3,5 @@ export * from './SchemaSettings';
export * from './hooks/useIsAllowToSetDefaultValue';
export { default as useParseDataScopeFilter } from './hooks/useParseDataScopeFilter';
export * from './VariableInput';

View File

@ -1,4 +1,4 @@
export { default as VariablesProvider } from './VariablesProvider';
export { default as VariablesProvider, VariablesContext } from './VariablesProvider';
export { default as useBuiltinVariables } from './hooks/useBuiltinVariables';
export { default as useContextVariable } from './hooks/useContextVariable';
export { default as useLocalVariables } from './hooks/useLocalVariables';

View File

@ -1,24 +1,27 @@
import { SchemaInitializerContext, useDesignable, useSchemaInitializer } from '@nocobase/client';
import { SchemaComponentOptions, SchemaInitializerContext, useSchemaInitializer } from '@nocobase/client';
import React, { useState } from 'react';
import { ChartConfigContext, ChartConfigCurrent, ChartConfigure } from '../configure/ChartConfigure';
import { ChartRendererProvider } from '../renderer';
import { ChartConfigProvider } from '../configure';
import { ChartDataProvider } from './ChartDataProvider';
import { ChartRenderer, ChartRendererProvider } from '../renderer';
import { ChartFilterBlockProvider, ChartFilterBlockDesigner } from '../filter';
import { ChartFilterProvider } from '../filter/FilterProvider';
export const ChartV2Block: React.FC = (props) => {
const { insertAdjacent } = useDesignable();
const [visible, setVisible] = useState(false);
const [current, setCurrent] = useState<ChartConfigCurrent>({} as any);
const [initialVisible, setInitialVisible] = useState(false);
const schemaInitializerContextData = useSchemaInitializer();
return (
<SchemaInitializerContext.Provider
value={{ ...schemaInitializerContextData, visible: initialVisible, setVisible: setInitialVisible }}
>
<ChartConfigContext.Provider value={{ visible, setVisible, current, setCurrent }}>
{props.children}
<ChartRendererProvider {...current.field?.decoratorProps}>
<ChartConfigure insert={(schema, options) => insertAdjacent('beforeEnd', schema, options)} />
</ChartRendererProvider>
</ChartConfigContext.Provider>
<SchemaComponentOptions
components={{ ChartRenderer, ChartRendererProvider, ChartFilterBlockProvider, ChartFilterBlockDesigner }}
>
<ChartDataProvider>
<ChartFilterProvider>
<ChartConfigProvider>{props.children}</ChartConfigProvider>
</ChartFilterProvider>
</ChartDataProvider>
</SchemaComponentOptions>
</SchemaInitializerContext.Provider>
);
};

View File

@ -1,38 +1,26 @@
import { LineChartOutlined } from '@ant-design/icons';
import { LineChartOutlined, BarChartOutlined } from '@ant-design/icons';
import { uid } from '@formily/shared';
import {
SchemaInitializerItem,
DataBlockInitializer,
SchemaInitializer,
SchemaInitializerItem,
useACLRoleContext,
useSchemaInitializerMenuItems,
useCollectionDataSourceItemsV2,
useSchemaInitializer,
useSchemaInitializerItem,
useMenuSearch,
} from '@nocobase/client';
import React, { useContext, useState } from 'react';
import { ChartConfigContext } from '../configure/ChartConfigure';
import { Menu } from 'antd';
import React, { useContext } from 'react';
import { ChartConfigContext } from '../configure';
import { lang } from '../locale';
import { FilterBlockInitializer } from '../filter';
const ConfigureButton = () => {
const itemConfig = useSchemaInitializerItem();
const ChartInitializer = () => {
const { setVisible, setCurrent } = useContext(ChartConfigContext);
return (
<SchemaInitializerItem
{...itemConfig}
applyMenuStyle={false}
onClick={() => {
setCurrent({ schema: {}, field: null, collection: itemConfig?.name, service: null, data: undefined });
setVisible(true);
}}
/>
);
};
const ItemsComponent = () => {
const collections = useCollectionDataSourceItemsV2('Chart');
const { allowAll, parseAction } = useACLRoleContext();
const items: any = collections
const itemConfig = useSchemaInitializerItem();
const items = collections
.filter((item) => {
if (allowAll) {
return true;
@ -42,17 +30,17 @@ const ItemsComponent = () => {
})
.map((item) => ({
...item,
Component: ConfigureButton,
}));
const [isOpenSubMenu, setIsOpenSubMenu] = useState(false);
const searchedChildren = useMenuSearch(items, isOpenSubMenu, true);
const menuItems = useSchemaInitializerMenuItems(searchedChildren);
return (
<Menu
items={menuItems}
onOpenChange={(keys) => {
setIsOpenSubMenu(keys.length > 0);
<DataBlockInitializer
{...itemConfig}
items={items}
icon={<BarChartOutlined />}
componentType={'Chart'}
onCreateBlockSchema={async ({ item }) => {
setCurrent({ schema: {}, field: null, collection: item.name, service: null, data: undefined });
setVisible(true);
}}
/>
);
@ -61,11 +49,26 @@ const ItemsComponent = () => {
export const chartInitializers = new SchemaInitializer({
name: 'ChartInitializers',
icon: 'PlusOutlined',
popoverProps: {
placement: 'bottomLeft',
},
title: '{{t("Add chart")}}',
ItemsComponent: ItemsComponent,
title: '{{t("Add block")}}',
items: [
{
name: 'chart',
title: lang('Chart'),
Component: ChartInitializer,
},
{
name: 'otherBlocks',
type: 'itemGroup',
title: lang('Other blocks'),
children: [
{
name: 'filter',
title: lang('Filter'),
Component: FilterBlockInitializer,
},
],
},
],
});
export const ChartV2BlockInitializer: React.FC = () => {

View File

@ -0,0 +1,29 @@
import React, { createContext, useState } from 'react';
import { useMemoizedFn } from 'ahooks';
type ChartData = {
collection: string;
service: any;
query: any;
};
export const ChartDataContext = createContext<{
charts: { [uid: string]: ChartData };
addChart: (uid: string, chart: ChartData) => void;
removeChart: (uid: string) => void;
}>({} as any);
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 removeChart = useMemoizedFn((uid: string) => {
setCharts((charts) => ({ ...charts, [uid]: undefined }));
});
return (
<ChartDataContext.Provider value={{ charts, addChart, removeChart }}>{props.children}</ChartDataContext.Provider>
);
};

View File

@ -15,9 +15,17 @@ export class Table extends AntdChart {
key: item,
}))
: [];
const rowKey = columns[0]?.dataIndex;
const dataSource = data.map((item: any) => {
Object.keys(item).map((key: string) => {
const props = fieldProps[key];
if (props?.interface === 'percent') {
const value = Math.round(parseFloat(item[key]) * 100).toFixed(2);
item[key] = `${value}%`;
}
if (typeof item[key] === 'boolean') {
item[key] = item[key].toString();
}
if (props?.transformer) {
item[key] = props.transformer(item[key]);
}
@ -28,14 +36,18 @@ export class Table extends AntdChart {
return {
bordered: true,
size: 'middle',
pagination:
dataSource.length < pageSize
? false
: {
pageSize,
},
// pagination:
// dataSource.length < pageSize
// ? false
// : {
// pageSize,
// },
dataSource,
columns,
scroll: {
x: 'max-content',
},
rowKey,
...general,
...advanced,
};

View File

@ -14,6 +14,7 @@ export type RenderProps = {
[field: string]: {
label: string;
transformer: Transformer;
interface: string;
};
};
};

View File

@ -0,0 +1,48 @@
import { ISchema } from '@formily/react';
import React, { createContext, useState } from 'react';
import { ChartRendererProvider } from '../renderer';
import { ChartConfigure } from './ChartConfigure';
import { useDesignable } from '@nocobase/client';
import { css } from '@emotion/css';
import { theme } from 'antd';
export type ChartConfigCurrent = {
schema: ISchema;
field: any;
collection: string;
service: any;
initialValues?: any;
data: any[];
};
export const ChartConfigContext = createContext<{
visible: boolean;
setVisible?: (visible: boolean) => void;
current?: ChartConfigCurrent;
setCurrent?: (current: ChartConfigCurrent) => void;
}>({
visible: true,
});
export const ChartConfigProvider: React.FC = (props) => {
const { insertAdjacent } = useDesignable();
const [visible, setVisible] = useState(false);
const [current, setCurrent] = useState<ChartConfigCurrent>({} as any);
const { token } = theme.useToken();
return (
<ChartConfigContext.Provider value={{ visible, setVisible, current, setCurrent }}>
<div
className={css`
.ant-card {
border: ${token.lineWidth}px ${token.lineType} ${token.colorBorderSecondary};
}
`}
>
{props.children}
</div>
<ChartRendererProvider {...current.field?.decoratorProps}>
<ChartConfigure insert={(schema, options) => insertAdjacent('beforeEnd', schema, options)} />
</ChartRendererProvider>
</ChartConfigContext.Provider>
);
};

View File

@ -13,7 +13,7 @@ import {
} 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, useMemo, useRef } from 'react';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import {
useChartFields,
useCollectionOptions,
@ -31,32 +31,14 @@ import { createRendererSchema, getField, getSelectedFields } from '../utils';
import { getConfigSchema, querySchema, transformSchema } from './schemas/configure';
import { useChartTypes, useCharts, useDefaultChartType } from '../chart/group';
import { FilterDynamicComponent } from './FilterDynamicComponent';
import { css } from '@emotion/css';
import { ChartConfigContext } from './ChartConfigProvider';
const { Paragraph, Text } = Typography;
export type ChartConfigCurrent = {
schema: ISchema;
field: any;
collection: string;
service: any;
initialValues?: any;
data: any[];
};
export type SelectedField = {
field: string | string[];
alias?: string;
};
export const ChartConfigContext = createContext<{
visible: boolean;
setVisible?: (visible: boolean) => void;
current?: ChartConfigCurrent;
setCurrent?: (current: ChartConfigCurrent) => void;
}>({
visible: true,
});
export const ChartConfigure: React.FC<{
insert: (
s: ISchema,
@ -151,7 +133,7 @@ export const ChartConfigure: React.FC<{
}
try {
await service.runAsync(collection, form.values.query);
await service.runAsync(collection, form.values.query, true);
} catch (e) {
console.log(e);
}
@ -164,6 +146,15 @@ export const ChartConfigure: React.FC<{
const queryRef = useRef(null);
const configRef = useRef(null);
useEffect(() => {
if (!visible) {
return;
}
service.run(collection, field?.decoratorProps?.query, 'configure');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);
return (
<Modal
title={t('Configure chart')}
@ -419,25 +410,19 @@ ChartConfigure.Data = function Data() {
const data = useData(current?.data);
const error = service?.error;
return !error ? (
<div
style={{
overflowX: 'auto',
overflowY: 'hidden',
}}
>
<Table
dataSource={data}
columns={Object.keys(data[0] || {}).map((col) => {
const field = getField(fields, col.split('.'));
return {
title: field?.label || col,
dataIndex: col,
key: col,
};
})}
size="small"
/>
</div>
<Table
dataSource={data}
scroll={{ x: 'max-content' }}
columns={Object.keys(data[0] || {}).map((col) => {
const field = getField(fields, col.split('.'));
return {
title: field?.label || col,
dataIndex: col,
key: col,
};
})}
size="small"
/>
) : (
<Alert
message="Error"

View File

@ -0,0 +1,2 @@
export * from './ChartConfigure';
export * from './ChartConfigProvider';

View File

@ -0,0 +1,168 @@
import { useForm } from '@formily/react';
import {
Action,
ActionInitializer,
GeneralSchemaDesigner,
SchemaInitializer,
SchemaSettingsDivider,
SchemaSettingsRemove,
} from '@nocobase/client';
import React, { useContext } from 'react';
import { useChartFilter } from '../hooks/filter';
import { ChartFilterContext } from './FilterProvider';
import { useChartsTranslation } from '../locale';
import { DownOutlined } from '@ant-design/icons';
export const useChartFilterActionProps = () => {
const { filter } = useChartFilter();
return {
onClick: filter,
};
};
export const useChartFilterResetProps = () => {
const form = useForm();
const { filter } = useChartFilter();
return {
onClick: async () => {
form.reset();
await filter();
},
};
};
export const useChartFilterCollapseProps = () => {
const {
collapse: { collapsed },
setCollapse,
} = useContext(ChartFilterContext);
const { t } = useChartsTranslation();
return {
onClick: () => setCollapse({ collapsed: !collapsed }),
title: (
<>
<DownOutlined rotate={!collapsed ? 180 : 0} /> {!collapsed ? t('Collapse') : t('Expand')}
</>
),
};
};
export const ChartFilterCollapseDesigner: React.FC = (props: any) => {
const { t } = useChartsTranslation();
return (
<GeneralSchemaDesigner {...props} disableInitializer>
<SchemaSettingsRemove
breakRemoveOn={(s) => {
return s['x-component'] === 'Space' || s['x-component'].endsWith('ActionBar');
}}
confirm={{
title: t('Delete action'),
}}
/>
</GeneralSchemaDesigner>
);
};
export const ChartFilterActionDesigner: React.FC = (props: any) => {
const { buttonEditorProps, ...restProps } = props;
const { t } = useChartsTranslation();
return (
<GeneralSchemaDesigner {...restProps} disableInitializer>
<Action.Designer.ButtonEditor {...buttonEditorProps} />
<SchemaSettingsDivider />
<SchemaSettingsRemove
breakRemoveOn={(s) => {
return s['x-component'] === 'Space' || s['x-component'].endsWith('ActionBar');
}}
confirm={{
title: t('Delete action'),
}}
/>
</GeneralSchemaDesigner>
);
};
const ChartFilterActionInitializer = (props) => {
const schema = {
title: '{{ t("Filter") }}',
'x-action': 'submit',
'x-component': 'Action',
'x-designer': 'ChartFilterActionDesigner',
'x-component-props': {
type: 'primary',
useProps: '{{ useChartFilterActionProps }}',
},
};
return <ActionInitializer {...props} schema={schema} />;
};
const ChartFilterResetInitializer = (props) => {
const schema = {
title: '{{ t("Reset") }}',
'x-action': 'reset',
'x-component': 'Action',
'x-designer': 'ChartFilterActionDesigner',
'x-component-props': {
useProps: '{{ useChartFilterResetProps }}',
},
};
return <ActionInitializer {...props} schema={schema} />;
};
const ChartFilterCollapseInitializer = (props) => {
const schema = {
title: `{{ t("Collapse") }}`,
'x-action': 'collapse',
'x-component': 'Action',
'x-component-props': {
type: 'link',
useProps: '{{ useChartFilterCollapseProps }}',
},
'x-designer': 'ChartFilterCollapseDesigner',
};
return <ActionInitializer {...props} schema={schema} />;
};
export const chartFilterActionInitializers = new SchemaInitializer({
name: 'ChartFilterActionInitializers',
'data-testid': 'configure-actions-button-of-chart-filter',
title: '{{t("Configure actions")}}',
icon: 'SettingOutlined',
items: [
{
name: 'enbaleActions',
type: 'itemGroup',
title: '{{t("Enable actions")}}',
children: [
{
name: 'filter',
type: 'item',
title: '{{t("Filter")}}',
component: ChartFilterActionInitializer,
schema: {
'x-action-settings': {},
},
},
{
name: 'reset',
type: 'item',
title: '{{t("Reset")}}',
component: ChartFilterResetInitializer,
schema: {
'x-action-settings': {},
},
},
{
name: 'collapse',
type: 'item',
title: '{{t("Collapse")}}',
component: ChartFilterCollapseInitializer,
schema: {
'x-action-settings': {},
},
},
],
},
],
});

View File

@ -0,0 +1,18 @@
import { GeneralSchemaDesigner, SchemaSettingsRemove } from '@nocobase/client';
import React from 'react';
import { useChartsTranslation } from '../locale';
export const ChartFilterBlockDesigner: React.FC = () => {
const { t } = useChartsTranslation();
return (
<GeneralSchemaDesigner disableInitializer title={t('Filter')}>
{/* <SchemaSettings.BlockTitleItem /> */}
{/* <SchemaSettings.Divider /> */}
<SchemaSettingsRemove
breakRemoveOn={{
'x-component': 'ChartV2Block',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -0,0 +1,110 @@
import React, { useContext } from 'react';
import { FilterOutlined } from '@ant-design/icons';
import {
Grid,
gridRowColWrap,
useDesignable,
useCurrentSchema,
SchemaInitializerSwitch,
useSchemaInitializerItem,
} from '@nocobase/client';
import { uid, merge } from '@formily/shared';
import { ChartFilterContext } from './FilterProvider';
import { css } from '@emotion/css';
import { theme } from 'antd';
const createFilterSchema = () => {
return {
type: 'void',
'x-action': 'filter',
'x-decorator': 'ChartFilterBlockProvider',
'x-component': 'CardItem',
'x-component-props': {
size: 'small',
},
'x-designer': 'ChartFilterBlockDesigner',
properties: {
[uid()]: {
type: 'void',
'x-component': 'ChartFilterForm',
properties: {
grid: {
type: 'void',
'x-component': 'ChartFilterGrid',
'x-initializer': 'ChartFilterItemInitializers',
properties: {},
},
actions: {
type: 'void',
'x-initializer': 'ChartFilterActionInitializers',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
float: 'right',
marginTop: 8,
},
},
properties: {},
},
},
},
},
};
};
export const ChartFilterGrid: React.FC = (props) => {
const {
collapse: { collapsed },
} = useContext(ChartFilterContext);
const { token } = theme.useToken();
return (
<div
className={css`
.ant-nb-grid {
overflow: hidden;
height: ${collapsed ? `${token.controlHeight * 2}px` : 'auto'};
}
`}
>
<Grid {...props}>{props.children}</Grid>
</div>
);
};
export const FilterBlockInitializer: React.FC = () => {
const { insertAdjacent } = useDesignable();
const { setEnabled } = useContext(ChartFilterContext);
const item = useSchemaInitializerItem();
const { remove: _remove, disabled } = item;
const type = 'x-action';
const schema = createFilterSchema();
const { exists, remove } = useCurrentSchema(
schema?.[type] || item?.schema?.[type],
type,
item.find,
_remove || item.remove,
);
return (
<SchemaInitializerSwitch
icon={<FilterOutlined />}
checked={exists}
disabled={disabled}
title={item.title}
onClick={() => {
if (disabled) {
return;
}
if (exists) {
setEnabled(false);
return remove();
}
const s = merge(schema || {}, item.schema || {});
item?.schemaInitialize?.(s);
insertAdjacent('afterBegin', gridRowColWrap(s));
setEnabled(true);
}}
/>
);
};

View File

@ -0,0 +1,53 @@
import React, { useContext, useEffect } from 'react';
import { SchemaComponentOptions } from '@nocobase/client';
import { ChartFilterItemDesigner } from './FilterItemDesigner';
import {
useChartFilterActionProps,
useChartFilterResetProps,
useChartFilterCollapseProps,
ChartFilterCollapseDesigner,
ChartFilterActionDesigner,
} from './FilterActionInitializers';
import { ChartFilterGrid } from './FilterBlockInitializer';
import { useChartsTranslation } from '../locale';
import { css } from '@emotion/css';
import { ChartFilterContext } from './FilterProvider';
import { ChartFilterCheckbox } from './FilterCheckbox';
import { ArrayItems } from '@formily/antd-v5';
import { ChartFilterFormItem } from './FilterItemInitializers';
import { ChartFilterForm } from './FilterForm';
export const ChartFilterBlockProvider: React.FC = (props) => {
const { t } = useChartsTranslation();
const { setEnabled } = useContext(ChartFilterContext);
useEffect(() => {
setEnabled(true);
}, [setEnabled]);
return (
<div
className={css`
.ant-card {
box-shadow: none;
border: none;
margin-bottom: 6px;
}
`}
>
<SchemaComponentOptions
components={{
ChartFilterItemDesigner,
ChartFilterForm,
ChartFilterGrid,
ChartFilterCheckbox,
ChartFilterFormItem,
ArrayItems,
ChartFilterCollapseDesigner,
ChartFilterActionDesigner,
}}
scope={{ t, useChartFilterActionProps, useChartFilterResetProps, useChartFilterCollapseProps }}
>
{props.children}
</SchemaComponentOptions>
</div>
);
};

View File

@ -0,0 +1,17 @@
import React from 'react';
import { connect, useField } from '@formily/react';
import { Checkbox } from 'antd';
import { Field } from '@formily/core';
export const ChartFilterCheckbox = connect((props) => {
const { content } = props;
const field = useField<Field>();
const handleClick = () => {
field.setValue(!field.value);
};
return (
<Checkbox onClick={handleClick} checked={field.value}>
{content}
</Checkbox>
);
});

View File

@ -0,0 +1,60 @@
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 { setDefaultValue } from './utils';
import { useChartFilter } from '../hooks';
export const ChartFilterForm: React.FC = memo((props) => {
const { setField, removeField, setForm } = useContext(ChartFilterContext);
const { getTranslatedTitle } = useChartFilter();
const variables = useRef<any>(null);
variables.current = useContext(VariablesContext);
const form = useMemo(
() =>
createForm({
effects() {
const getField = (field: any) => {
if (field.displayName !== 'Field') {
return null;
}
const { name } = field.props || {};
return name;
};
onFieldInit('*', (field: any) => {
const name = getField(field);
if (!name) {
return;
}
field.setValue(null);
});
onFieldMount('*', async (field: any) => {
const name = getField(field);
if (!name) {
return;
}
setField(name, { title: field.title, operator: field.componentProps['filter-operator'] });
// parse field title
if (field.title.includes('/')) {
field.title = getTranslatedTitle(field.title);
}
// parse default value
setDefaultValue(field, variables.current);
});
onFieldUnmount('*', (field: any) => {
const name = getField(field);
if (!name) {
return;
}
removeField(name);
});
},
}),
[setField, getTranslatedTitle, removeField, variables],
);
useEffect(() => setForm(form), [form, setForm]);
return <FormV2 {...props} form={form} />;
});

View File

@ -0,0 +1,323 @@
import React, { useContext } from 'react';
import {
EditDescription,
GeneralSchemaDesigner,
SchemaSettingsItem,
SchemaSettingsDivider,
SchemaSettingsModalItem,
SchemaSettingsRemove,
VariablesContext,
useCollection,
useCollectionManager,
useCompile,
useDesignable,
SchemaSettingsSelectItem,
} from '@nocobase/client';
import { useChartsTranslation } from '../locale';
import { Schema, useField, useFieldSchema } from '@formily/react';
import { Field } from '@formily/core';
import _ from 'lodash';
import { ChartFilterContext } from './FilterProvider';
import { getPropsSchemaByComponent, setDefaultValue } from './utils';
import { ChartFilterVariableInput } from './FilterVariableInput';
import { useChartFilter, useCollectionJoinFieldTitle } from '../hooks';
import { Typography } from 'antd';
const { Text } = Typography;
const EditTitle = () => {
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useChartsTranslation();
const { dn } = useDesignable();
const { setField } = useContext(ChartFilterContext);
return (
<SchemaSettingsModalItem
key="edit-field-title"
title={t('Edit field title')}
schema={{
type: 'object',
title: t('Edit field title'),
properties: {
title: {
title: t('Field title'),
default: field?.title,
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
},
}}
onSubmit={({ title }) => {
if (title) {
field.title = title;
fieldSchema.title = title;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
title: fieldSchema.title,
},
});
setField(fieldSchema.name as string, { title });
}
dn.refresh();
}}
/>
);
};
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();
let props = getCollectionJoinField(fieldName);
let interfaceConfig = getInterface(props?.interface);
let operatorList = interfaceConfig?.filterable?.operators || [];
if (!operatorList.length) {
const names = fieldName.split('.');
const name = names.pop();
props = getCollectionJoinField(names.join('.'));
if (!props) {
return null;
}
interfaceConfig = getInterface(props.interface);
if (!interfaceConfig) {
return null;
}
const children = interfaceConfig?.filterable.children || [];
const child = children.find((item: any) => item.name === name);
operatorList = child?.operators || [];
}
if (!operatorList.length) {
return null;
}
const defaultComponent = interfaceConfig?.default?.uiSchema?.['x-component'] || 'Input';
const operator = fieldSchema['x-component-props']?.['filter-operator'];
const setOperatorComponent = (operator: any, component: any, props = {}) => {
const componentProps = field.componentProps || {};
field.component = component;
field.componentProps = {
...componentProps,
'filter-operator': operator,
...props,
};
fieldSchema['x-component'] = component;
fieldSchema['x-component-props'] = {
...fieldSchema['x-component-props'],
'filter-operator': operator,
...props,
};
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-component': component,
'x-component-props': {
...fieldSchema['x-component-props'],
'filter-operator': operator,
...props,
},
},
});
};
return (
<SchemaSettingsSelectItem
key="operator"
title={t('Operator')}
value={operator?.value || operatorList[0]?.value}
options={compile(operatorList)}
onChange={(op: string) => {
const operator = operatorList.find((item: any) => item.value === op);
if (operator.noValue) {
setOperatorComponent(operator, 'ChartFilterCheckbox', {
content: Schema.compile(operator.label, { t }),
});
} else if (operator.schema?.['x-component']) {
setOperatorComponent(operator, operator.schema['x-component']);
} else {
setOperatorComponent(operator, defaultComponent);
}
setField(fieldName, { operator });
dn.refresh();
}}
/>
);
};
const EditProps = () => {
const { t } = useChartsTranslation();
const { dn } = useDesignable();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const propsSchema = getPropsSchemaByComponent(fieldSchema['x-component']);
return (
<SchemaSettingsModalItem
key="edit-field-props"
title={t('Edit field properties')}
schema={{
title: t('Edit field properties'),
...propsSchema,
}}
initialValues={field.componentProps}
onSubmit={(props) => {
field.reset();
field.componentProps = props;
fieldSchema['x-component-props'] = props;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-component-props': props,
},
});
dn.refresh();
}}
/>
);
};
const EditDefaultValue = () => {
const { t } = useChartsTranslation();
const { dn } = useDesignable();
const variables = useContext(VariablesContext);
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { getTranslatedTitle } = useChartFilter();
const title = getTranslatedTitle(fieldSchema.title);
return (
<SchemaSettingsModalItem
key="set field default value"
title={t('Set default value')}
components={{
ChartFilterVariableInput,
}}
schema={{
type: 'void',
title: t('Set default value'),
properties: {
default: {
title,
'x-decorator': 'FormItem',
'x-component': 'ChartFilterVariableInput',
'x-component-props': {
fieldSchema,
},
},
},
}}
onSubmit={({ default: { value } }) => {
field.setInitialValue(value);
fieldSchema.default = value;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
default: value,
},
});
dn.refresh();
setDefaultValue(field, variables);
}}
/>
);
};
const EditTitleField = () => {
const { getCollectionFields, getCollectionJoinField, getInterface } = useCollectionManager();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useChartsTranslation();
const { dn } = useDesignable();
const compile = useCompile();
const collectionField = getCollectionJoinField(fieldSchema['x-collection-field']);
const targetFields = collectionField?.target
? getCollectionFields(collectionField?.target)
: getCollectionFields(collectionField?.targetCollection) ?? [];
const options = targetFields
.filter((field) => {
if (field?.target || field.type === 'boolean') {
return false;
}
const fieldInterface = getInterface(field?.interface);
return fieldInterface?.titleUsable;
})
.map((field) => ({
value: field?.name,
label: compile(field?.uiSchema?.title) || field?.name,
}));
return options.length > 0 && fieldSchema['x-component'] === 'CollectionField' ? (
<SchemaSettingsSelectItem
key="title-field"
title={t('Title field')}
options={options}
value={field?.componentProps?.fieldNames?.label}
onChange={(label: string) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
const fieldNames = {
...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
...field.componentProps.fieldNames,
label,
};
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
schema['x-component-props'] = fieldSchema['x-component-props'];
field.componentProps.fieldNames = fieldSchema['x-component-props'].fieldNames;
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
) : null;
};
export const ChartFilterItemDesigner: React.FC = () => {
const { getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const { t } = useChartsTranslation();
const fieldSchema = useFieldSchema();
const fieldName = fieldSchema.name as string;
const collectionField = getField(fieldName) || getCollectionJoinField(fieldSchema['x-collection-field']);
const isCustom = fieldName.startsWith('custom.');
const hasProps = getPropsSchemaByComponent(fieldSchema['x-component']);
const originalTitle = useCollectionJoinFieldTitle(fieldName);
return (
<GeneralSchemaDesigner disableInitializer>
{!isCustom && (
<>
<SchemaSettingsItem title={fieldName}>
<Text type="secondary">
{t('Original field')}: {originalTitle}
</Text>
</SchemaSettingsItem>
<SchemaSettingsDivider />
</>
)}
<EditTitle />
<EditDescription />
{hasProps && isCustom && <EditProps />}
{!isCustom && <EditOperator />}
<EditTitleField />
<EditDefaultValue />
{collectionField ? <SchemaSettingsDivider /> : null}
<SchemaSettingsRemove
key="remove"
confirm={{
title: t('Delete field'),
}}
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -0,0 +1,243 @@
import {
ACLCollectionFieldProvider,
BlockItem,
FormDialog,
HTMLEncode,
SchemaComponent,
SchemaComponentOptions,
SchemaInitializer,
SchemaInitializerItem,
gridRowColWrap,
useCollectionManager,
useDesignable,
useGlobalTheme,
useSchemaInitializerItem,
} from '@nocobase/client';
import React, { useCallback, useContext, useMemo } from 'react';
import { lang, useChartsTranslation } from '../locale';
import { Schema, SchemaOptionsContext, observer, useField, useFieldSchema, useForm } from '@formily/react';
import { useMemoizedFn } from 'ahooks';
import { FormLayout, FormItem } from '@formily/antd-v5';
import { uid } from '@formily/shared';
import { useChartData, useChartFilter, useChartFilterSourceFields, useFieldComponents } from '../hooks/filter';
import { Alert } from 'antd';
import { getPropsSchemaByComponent } from './utils';
import { Field, onFieldValueChange } from '@formily/core';
import { css, cx } from '@emotion/css';
const FieldComponentProps: React.FC = observer((props) => {
const form = useForm();
const schema = getPropsSchemaByComponent(form.values.component);
return schema ? <SchemaComponent schema={schema} {...props} /> : null;
});
export const ChartFilterFormItem = observer(
(props: any) => {
const field = useField<Field>();
const schema = useFieldSchema();
const showTitle = schema['x-decorator-props']?.showTitle ?? true;
const extra = useMemo(() => {
return typeof field.description === 'string' ? (
<div
dangerouslySetInnerHTML={{
__html: HTMLEncode(field.description).split('\n').join('<br/>'),
}}
/>
) : (
field.description
);
}, [field.description]);
const className = useMemo(() => {
return cx(
css`
& .ant-space {
flex-wrap: wrap;
}
`,
{
[css`
> .ant-formily-item-label {
display: none;
}
`]: showTitle === false,
},
);
}, [showTitle]);
return (
<ACLCollectionFieldProvider>
<BlockItem className={'nb-form-item'}>
<FormItem className={className} {...props} extra={extra} />
</BlockItem>
</ACLCollectionFieldProvider>
);
},
{ displayName: 'ChartFilterFormItem' },
);
export const ChartFilterCustomItemInitializer: React.FC<{
insert?: any;
}> = (props) => {
const { t: lang } = useChartsTranslation();
const t = useMemoizedFn(lang);
const { scope, components } = useContext(SchemaOptionsContext);
const { theme } = useGlobalTheme();
const { insert } = props;
const itemConfig = useSchemaInitializerItem();
const { getCollectionJoinField, getInterface } = useCollectionManager();
const sourceFields = useChartFilterSourceFields();
const { options: fieldComponents, values: fieldComponentValues } = useFieldComponents();
const handleClick = useCallback(async () => {
const values = await FormDialog(
t('Add custom field'),
() => (
<SchemaComponentOptions
scope={{ ...scope, useChartFilterSourceFields }}
components={{ ...components, FieldComponentProps }}
>
<FormLayout layout={'vertical'}>
<Alert
type="info"
message={t('To filter with custom fields, use "Current filter" variables in the chart configuration.')}
style={{ marginBottom: 16 }}
/>
<SchemaComponent
schema={{
properties: {
name: {
type: 'string',
required: true,
},
title: {
type: 'string',
title: t('Field title'),
'x-component': 'Input',
'x-decorator': 'FormItem',
required: true,
},
source: {
type: 'string',
title: t('Field source'),
'x-decorator': 'FormItem',
'x-component': 'Cascader',
enum: sourceFields,
description: t('Select a source field to use metadata of the field'),
},
component: {
type: 'string',
title: t('Field component'),
'x-component': 'Select',
'x-decorator': 'FormItem',
required: true,
enum: fieldComponents,
},
props: {
type: 'object',
title: t('Component properties'),
'x-component': 'FieldComponentProps',
},
},
}}
/>
</FormLayout>
</SchemaComponentOptions>
),
theme,
).open({
values: {
name: `f_${uid()}`,
},
effects() {
onFieldValueChange('source', (field) => {
const name = field.value?.join('.');
const props = getCollectionJoinField(name);
if (!props) {
return;
}
const uiSchema = props.uiSchema || {};
let fieldComponent: string;
if (fieldComponentValues.includes(uiSchema['x-component'])) {
fieldComponent = uiSchema['x-component'];
const fieldComponentProps = uiSchema['x-component-props'] || {};
if (uiSchema.enum) {
fieldComponentProps.options = uiSchema.enum;
}
const componentProps = field.query('.props').take() as Field;
componentProps.setValue(fieldComponentProps);
} else if (fieldComponentValues.includes(props.interface)) {
fieldComponent = props.interface;
}
if (!fieldComponent) {
return;
}
const component = field.query('.component').take() as Field;
component.setValue(fieldComponent);
});
},
});
const { name, title, component, props } = values;
const defaultSchema = getInterface(component)?.default?.uiSchema || {};
insert(
gridRowColWrap({
'x-component': component,
...defaultSchema,
type: 'string',
title: title,
name: `custom.${name}`,
required: false,
'x-designer': 'ChartFilterItemDesigner',
'x-decorator': 'ChartFilterFormItem',
'x-component-props': {
...(defaultSchema['x-component-props'] || {}),
...props,
},
}),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [theme]);
return <SchemaInitializerItem {...itemConfig} {...props} onClick={handleClick} />;
};
export const chartFilterItemInitializers = new SchemaInitializer({
name: 'ChartFilterItemInitializers',
'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();
const { getChartCollections } = useChartData();
const { getChartFilterFields } = useChartFilter();
const collections = getChartCollections();
return collections.map((name: any) => {
const collection = getCollection(name);
const fields = getChartFilterFields(collection);
return {
name: collection.key,
type: 'subMenu',
title: collection.title,
children: fields,
};
});
},
},
{
name: 'divider',
type: 'divider',
},
{
name: 'custom',
type: 'item',
title: lang('Custom'),
Component: () => {
const { insertAdjacent } = useDesignable();
return <ChartFilterCustomItemInitializer insert={(s: Schema) => insertAdjacent('beforeEnd', s)} />;
},
},
],
});

View File

@ -0,0 +1,68 @@
import React, { createContext, useEffect, useState } from 'react';
import { useMemoizedFn } from 'ahooks';
export const ChartFilterContext = createContext<{
ready: boolean;
enabled: boolean;
setEnabled: (enabled: boolean) => void;
fields: {
[name: string]: {
title: string;
operator?: {
value: string;
noValue?: boolean;
};
};
};
setField: (name: string, field: { title?: string; operator?: string }) => void;
removeField: (name: string) => void;
collapse: {
collapsed: boolean;
row: number;
};
setCollapse: (opts: { collapsed?: boolean; row?: number }) => void;
form: any;
setForm: (form: any) => void;
}>({} as any);
export const ChartFilterProvider: React.FC = (props) => {
const [ready, setReady] = useState(false);
const [enabled, _setEnabled] = useState(false);
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 }) => {
setFields((fields) => ({
...fields,
[name]: {
...(fields[name] || {}),
...props,
},
}));
});
const removeField = useMemoizedFn((name: string) => {
setFields((fields) => {
const newFields = {
...fields,
};
newFields[name] = undefined;
return newFields;
});
});
const setForm = useMemoizedFn(_setForm);
const setEnabled = useMemoizedFn(_setEnabled);
const setCollapse = ({ collapsed, row }: { collapsed?: boolean; row?: number }) => {
_setCollapse((collapse) => ({
collapsed: collapsed !== undefined ? collapsed : collapse.collapsed,
row: row !== undefined ? row : collapse.row,
}));
};
useEffect(() => setReady(true), []);
return (
<ChartFilterContext.Provider
value={{ ready, enabled, setEnabled, fields, setField, removeField, collapse, setCollapse, form, setForm }}
>
{props.children}
</ChartFilterContext.Provider>
);
};

View File

@ -0,0 +1,56 @@
import {
SchemaComponent,
Variable,
VariableInput,
VariableScopeProvider,
getShouldChange,
useDateVariable,
useUserVariable,
} from '@nocobase/client';
import React, { useEffect, useMemo } from 'react';
import { useMemoizedFn } from 'ahooks';
export const ChartFilterVariableInput: React.FC<any> = (props) => {
const { value, onChange, fieldSchema } = props;
const userVariable = useUserVariable({
collectionField: { uiSchema: fieldSchema },
uiSchema: fieldSchema,
});
const dateVariable = useDateVariable({
operator: fieldSchema['x-component-props']?.['filter-operator'],
schema: fieldSchema,
noDisabled: true,
});
const options = useMemo(() => [userVariable, dateVariable].filter(Boolean), [dateVariable, userVariable]);
const schema = {
...fieldSchema,
'x-component': fieldSchema['x-component'] || 'Input',
'x-decorator': '',
title: '',
name: 'value',
default: '',
};
const componentProps = fieldSchema['x-component-props'] || {};
const handleChange = useMemoizedFn(onChange);
useEffect(() => {
if (fieldSchema.default) {
handleChange({ value: fieldSchema.default });
}
}, [fieldSchema.default, handleChange]);
return (
<VariableScopeProvider scope={options}>
<VariableInput
{...componentProps}
renderSchemaComponent={() => <SchemaComponent schema={schema} />}
fieldNames={{}}
value={value?.value}
scope={options}
onChange={(v: any) => {
onChange({ value: v });
}}
shouldChange={getShouldChange({} as any)}
/>
</VariableScopeProvider>
);
};

View File

@ -0,0 +1,6 @@
export * from './FilterBlockDesigner';
export * from './FilterBlockInitializer';
export * from './FilterItemInitializers';
export * from './FilterActionInitializers';
export * from './FilterItemDesigner';
export * from './FilterBlockProvider';

View File

@ -0,0 +1,177 @@
import { moment2str } from '@nocobase/utils';
import dayjs from 'dayjs';
export const getOptionsSchema = () => {
const options = {
title: '{{t("Options")}}',
type: 'array',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems',
items: {
type: 'object',
'x-decorator': 'ArrayItems.Item',
properties: {
space: {
type: 'void',
'x-component': 'Space',
properties: {
label: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Option label")}}',
},
required: true,
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Option value")}}',
},
required: true,
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: 'Add',
'x-component': 'ArrayItems.Addition',
},
},
};
return options;
};
export const getPropsSchemaByComponent = (component: string) => {
const showTime = {
type: 'boolean',
'x-content': '{{ t("Show time") }}',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
};
const dateFormat = {
type: 'string',
title: '{{t("Date format")}}',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
default: 'YYYY-MM-DD',
enum: [
{
label: '{{t("Year/Month/Day")}}',
value: 'YYYY/MM/DD',
},
{
label: '{{t("Year-Month-Day")}}',
value: 'YYYY-MM-DD',
},
{
label: '{{t("Day/Month/Year")}}',
value: 'DD/MM/YYYY',
},
],
};
const options = getOptionsSchema();
const propsSchema = {
DatePicker: {
type: 'object',
properties: {
dateFormat,
showTime,
},
},
'DatePicker.RangePicker': {
type: 'object',
properties: {
dateFormat,
showTime,
},
},
Select: {
type: 'object',
properties: {
mode: {
type: 'string',
enum: [
{
label: '{{ t("Single select") }}',
value: '',
},
{
label: '{{ t("Multiple select") }}',
value: 'multiple',
},
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {
defaultValue: '',
},
},
options,
},
},
'Checkbox.Group': {
type: 'object',
properties: {
options,
},
},
'Radio.Group': {
type: 'object',
properties: {
options,
},
},
};
return propsSchema[component];
};
export const transformValue = (value: any, props: any) => {
if (!value) {
return value;
}
if (dayjs.isDayjs(value)) {
if (!props.showTime) {
value = value.startOf('day');
}
return moment2str(value, props);
}
if (Array.isArray(value) && value.length && dayjs.isDayjs(value[0])) {
let start = value[0];
let end = value[1];
if (props.showTime) {
start = start.startOf('day');
end = end.endOf('day');
}
return [moment2str(start, props), moment2str(end, props)];
}
return value;
};
export const setDefaultValue = async (field: any, variables: any) => {
const defaultValue = field.initialValue;
const isVariable =
typeof defaultValue === 'string' && defaultValue?.startsWith('{{$') && defaultValue?.endsWith('}}');
if (!isVariable || !variables) {
field.setValue(defaultValue);
field.setInitialValue(defaultValue);
} else {
field.loading = true;
const value = await variables.parseVariable(defaultValue);
const transformedValue = transformValue(value, field.componentProps);
field.setValue(transformedValue);
field.setInitialValue(transformedValue);
field.loading = false;
}
};

View File

@ -0,0 +1,485 @@
import { SchemaInitializerItemType, i18n, useActionContext, useCollectionManager } from '@nocobase/client';
import { 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';
import { useMemoizedFn } from 'ahooks';
import { parse } from '@nocobase/utils';
import lodash from 'lodash';
import { getValuesByPath } from '../utils';
import deepmerge from 'deepmerge';
export const useCustomFieldInterface = () => {
const { getInterface } = useCollectionManager();
return {
getSchemaByInterface: (fieldInterface: string) => {
const interfaceConfig = getInterface(fieldInterface);
const defaultSchema = interfaceConfig?.default.uiSchema;
const schema = {
...defaultSchema,
};
switch (fieldInterface) {
case 'datetime':
return {
...schema,
'x-component-props': {
...defaultSchema['x-component-props'],
showTime: true,
},
};
default:
return {
...schema,
'x-component-props': {
...defaultSchema['x-component-props'],
},
};
}
},
};
};
export const useChartData = () => {
const { charts } = useContext(ChartDataContext);
const getChartCollections = () =>
Array.from(
new Set(
Object.values(charts)
.filter((chart) => chart)
.map((chart) => chart.collection),
),
);
return {
getChartCollections,
};
};
export const useChartFilter = () => {
const { charts } = useContext(ChartDataContext);
const { fieldSchema } = useActionContext();
const action = fieldSchema?.['x-action'];
const { getCollection, getInterface, getCollectionFields, getCollectionJoinField } = useCollectionManager();
const { fields: fieldProps, form } = useContext(ChartFilterContext);
const getChartFilterFields = (collection: CollectionOptions) => {
const fields = getCollectionFields(collection);
const field2item = (field: any, title: string, name: string) => {
const fieldTitle = field.uiSchema?.title || field.name;
const interfaceConfig = getInterface(field.interface);
const defaultOperator = interfaceConfig?.filterable?.operators?.[0];
const targetCollection = getCollection(field.target);
title = title ? `${title} / ${fieldTitle}` : fieldTitle;
let schema = {
type: 'string',
title,
name: `${name}.${field.name}`,
required: false,
'x-designer': 'ChartFilterItemDesigner',
'x-component': 'CollectionField',
'x-decorator': 'ChartFilterFormItem',
'x-collection-field': `${name}.${field.name}`,
'x-component-props': {
...field.uiSchema?.['x-component-props'],
'filter-operator': defaultOperator,
},
};
if (field.interface === 'formula') {
schema = {
...schema,
'x-component': 'InputNumber',
};
}
if (['oho', 'o2m'].includes(field.interface)) {
schema['x-component-props'].useOriginalFilter = true;
}
const resultItem: SchemaInitializerItemType = {
key: `${name}.${field.name}`,
name: field.name,
type: 'item',
title: field?.uiSchema?.title || field.name,
Component: 'CollectionFieldInitializer',
remove: (schema, cb) => {
cb(schema, {
breakRemoveOn: {
'x-component': 'Grid',
},
});
},
schemaInitialize: (s: any) => {
interfaceConfig?.schemaInitialize?.(s, {
field,
block: 'FilterForm',
readPretty: form.readPretty,
action,
targetCollection,
});
},
schema,
};
return resultItem;
};
const children2item = (child: any, title: string, name: string) => {
const childTitle = child.uiSchema?.title || child.name;
title = title ? `${title} / ${childTitle}` : childTitle;
const defaultOperator = child.operators[0];
let schema = {
type: 'string',
name: `${name}.${child.name}`,
required: false,
'x-designer': 'ChartFilterItemDesigner',
'x-decorator': 'ChartFilterFormItem',
'x-collection-field': `${name}.${child.name}`,
...child.schema,
title,
'x-component-props': {
'filter-operator': defaultOperator,
},
};
if (defaultOperator?.noValue) {
schema = {
...schema,
'x-component': 'ChartFilterCheckbox',
'x-component-props': {
...schema['x-component-props'],
content: Schema.compile(defaultOperator.label, { t: i18n.t }),
},
};
}
const resultItem: SchemaInitializerItemType = {
key: `${name}.${child.name}`,
name: child.name,
type: 'item',
title: child.title || child.name,
Component: 'CollectionFieldInitializer',
remove: (schema, cb) => {
cb(schema, {
breakRemoveOn: {
'x-component': 'Grid',
},
});
},
schema,
};
return resultItem;
};
const field2option = (field: any, depth: number, title: string, name: string): SchemaInitializerItemType => {
if (!field.interface) {
return;
}
const fieldInterface = getInterface(field.interface);
if (!fieldInterface?.filterable) {
return;
}
const { nested, children } = fieldInterface.filterable;
const fieldTitle = field.uiSchema?.title || field.name;
const item = field2item(field, title, name);
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}`));
return {
key: `${name}.${field.name}`,
name: field.name,
type: 'subMenu',
title: field?.uiSchema?.title || field.name,
children: items,
};
}
if (!depth || depth > 2) {
return item;
}
if (nested) {
const targetFields = getCollectionFields(field.target);
const items = targetFields.map((targetField) =>
field2option(targetField, depth + 1, '', `${name}.${field.name}`),
);
return {
key: `${name}.${field.name}`,
name: field.name,
type: 'subMenu',
title: field?.uiSchema?.title || field.name,
children: items.filter(Boolean),
};
}
return item;
};
const options: SchemaInitializerItemType[] = [];
const associationOptions = [];
fields.forEach((field) => {
const fieldInterface = field.interface;
const option = field2option(field, 0, '', collection.name);
if (option) {
options.push(option);
}
if (['m2o'].includes(fieldInterface)) {
const option = field2option(field, 1, '', collection.name);
if (option) {
associationOptions.push(option);
}
}
});
if (associationOptions.length) {
options.push(
{
name: 'divider',
type: 'divider',
},
{
name: 'displayAssociationFields',
type: 'itemGroup',
title: i18n.t('Display association fields'),
children: associationOptions,
},
);
}
return options;
};
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;
}
});
return filter;
};
const hasFilter = (chart: { collection: string; query: any }, filterValues: any) => {
const { collection, query } = chart;
const { parameters } = parse(query.filter || '');
return (
chart &&
(filterValues[collection] ||
(filterValues['custom'] && parameters?.find((param: { key: string }) => filterValues['custom'][param.key])))
);
};
const appendFilter = (chart: { collection: string; query: any }, filterValues: any) => {
const { collection, query } = chart;
let newQuery = { ...query };
const originFilter = { ...(newQuery.filter || {}) };
let filter = {};
const parsed = parse(originFilter);
const { parameters } = parsed;
if (filterValues['custom'] && parameters?.find((param: { key: string }) => filterValues['custom'][param.key])) {
filter = parsed(filterValues['custom']);
}
filter = deepmerge(originFilter, filter);
newQuery = {
...newQuery,
filter: {
$and: [filter, filterValues[collection]],
},
};
return newQuery;
};
const filter = async () => {
const filterValues = getFilter();
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);
});
await Promise.all(requests.map((request) => request()));
};
const refresh = async () => {
const requests = Object.values(charts)
.filter((chart) => {
return chart;
})
.map((chart) => async () => {
const { service, collection, query } = chart;
await service.runAsync(collection, query, true);
});
await Promise.all(requests.map((request) => request()));
};
const getTranslatedTitle = useMemoizedFn((title: string) => {
return title
.split(' / ')
.map((item: string) => i18n.t(Schema.compile(item, { t: i18n.t })))
.join(' / ');
});
return {
filter,
refresh,
getChartFilterFields,
getFilter,
hasFilter,
appendFilter,
getTranslatedTitle,
};
};
export const useFilterVariable = () => {
const { t: trans } = useChartsTranslation();
const t = useMemoizedFn(trans);
const { enabled, fields } = useContext(ChartFilterContext);
const options = Object.entries(fields)
.filter(([name, value]) => name.startsWith('custom.') && value)
.map(([name, { title }]) => {
const value = name.replace(/^custom\./, '');
return {
key: value,
value,
label: title,
};
});
const result = useMemo(
() => ({
label: t('Current filter'),
value: '$nFilter',
key: '$nFilter',
children: options,
}),
[options, t],
);
if (!enabled || !options.length) return null;
return result;
};
export const useChartFilterSourceFields = () => {
const { t } = useChartsTranslation();
const { getChartCollections } = useChartData();
const { getInterface, getCollectionFields, getCollection } = useCollectionManager();
const { values } = useFieldComponents();
const field2option = (field: any, depth: number) => {
if (!field.interface) {
return;
}
const fieldInterface = getInterface(field.interface);
if (!fieldInterface?.filterable) {
return;
}
const { nested } = fieldInterface.filterable;
const item = {
value: field.name,
label: t(field?.uiSchema?.title || field.name),
};
if (field.target && depth > 2) {
return;
}
if (depth > 2) {
return item;
}
if (nested) {
const targetFields = getCollectionFields(field.target);
const items = targetFields.map((targetField) => field2option(targetField, depth + 1));
return {
value: field.name,
label: t(field?.uiSchema?.title || field.name),
children: items.filter((item: any) => item),
};
}
if (!values.includes(field.uiSchema?.['x-component']) && !values.includes(field.interface)) {
return;
}
return item;
};
const collections = getChartCollections();
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,
});
}
});
return options;
};
export const useFieldComponents = () => {
const { t } = useChartsTranslation();
const options = [
{ label: t('Input'), value: 'Input' },
{ label: t('Number'), value: 'InputNumber' },
{ label: t('Date'), value: 'DatePicker' },
{ label: t('Date range'), value: 'DatePicker.RangePicker' },
{ label: t('Time'), value: 'TimePicker' },
{ label: t('Time range'), value: 'TimePicker.RangePicker' },
{ label: t('Select'), value: 'Select' },
{ label: t('Radio group'), value: 'Radio.Group' },
{ label: t('Checkbox group'), value: 'Checkbox.Group' },
// { label: t('China region'), value: 'chinaRegion' },
];
return {
options,
values: options.map((option) => option.value),
};
};
export const useCollectionJoinFieldTitle = (name: string) => {
const { getCollection, getCollectionField } = useCollectionManager();
return useMemo(() => {
if (!name) {
return;
}
const [collectionName, ...fieldNames] = name.split('.');
if (!fieldNames?.length) {
return;
}
const collection = getCollection(collectionName);
let cName: any = collectionName;
let field: any;
let title = Schema.compile(collection?.title, { t: i18n.t });
while (cName && fieldNames.length > 0) {
const fileName = fieldNames.shift();
field = getCollectionField(`${cName}.${fileName}`);
const fieldTitle = field?.uiSchema?.title || field?.name;
if (fieldTitle) {
title += ` / ${Schema.compile(fieldTitle, { t: i18n.t })}`;
}
if (field?.target) {
cName = field.target;
} else {
cName = null;
}
}
return title;
}, [name]);
};

View File

@ -1,3 +1,4 @@
export * from './query';
export * from './transformer';
export * from './useVariableOptions';
export * from './filter';

View File

@ -3,7 +3,7 @@ import { ISchema, Schema, useForm } from '@formily/react';
import { CollectionFieldOptions, useACLRoleContext, useCollectionManager } from '@nocobase/client';
import { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { ChartConfigContext } from '../configure/ChartConfigure';
import { ChartConfigContext } from '../configure';
import formatters from '../block/formatters';
import { useChartsTranslation } from '../locale';
import { ChartRendererContext } from '../renderer';

View File

@ -2,19 +2,19 @@ import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useMemoizedFn } from 'ahooks';
export const useDateVariable = ({ operator, schema }) => {
export const useDateVariable = ({ schema }) => {
const { t: trans } = useTranslation();
const t = useMemoizedFn(trans);
const operatorValue = operator?.value || '';
const component = schema?.['x-component'];
const disabled = !['DatePicker', 'DatePicker.RangePicker'].includes(component);
// const disabled = !['DatePicker', 'DatePicker.RangePicker'].includes(component);
const disabled = false;
const dateOptions = useMemo(
() => [
{
key: 'now',
value: 'now',
label: t('Now'),
disabled: component !== 'DatePicker' || operatorValue === '$dateBetween',
disabled,
},
{
key: 'yesterday',
@ -143,7 +143,7 @@ export const useDateVariable = ({ operator, schema }) => {
disabled,
},
],
[disabled, operatorValue, t, component],
[disabled, t],
);
const result = useMemo(
@ -157,7 +157,7 @@ export const useDateVariable = ({ operator, schema }) => {
[dateOptions, t],
);
if (!operator || !schema) return null;
if (!schema) return null;
return result;
};

View File

@ -15,7 +15,7 @@ export const useUserVariable = ({ schema }: { schema: any }) => {
const result = {
key: option.name,
value: option.name,
disabled: !option.children && component !== option.schema?.['x-component'],
// disabled: !option.children && component !== option.schema?.['x-component'],
label: Schema.compile(option.title, { t }),
};
if (option.children) {
@ -23,7 +23,7 @@ export const useUserVariable = ({ schema }: { schema: any }) => {
}
return result;
}),
[t, component],
[t],
);
const children = useMemo(() => getOptions(options), [getOptions, options]);
const result = useMemo(

View File

@ -1,15 +1,22 @@
import { useField } from '@formily/react';
import { useMemo } from 'react';
import { useDateVariable } from './useDateVariable';
import { useUserVariable } from './useUserVariable';
import { useFilterVariable } from './filter';
import { useDateVariable, useUserVariable } from '@nocobase/client';
export const useVariableOptions = () => {
const field = useField<any>();
const { operator, schema } = field.data || {};
const userVariable = useUserVariable({ schema });
const userVariable = useUserVariable({
collectionField: { uiSchema: schema },
uiSchema: schema,
});
const dateVariable = useDateVariable({ operator, schema });
const filterVariable = useFilterVariable();
const result = useMemo(() => [userVariable, dateVariable].filter(Boolean), [dateVariable, userVariable]);
const result = useMemo(
() => [userVariable, dateVariable, filterVariable].filter(Boolean),
[dateVariable, userVariable, filterVariable],
);
if (!operator || !schema) return [];

View File

@ -3,7 +3,9 @@ import { ChartV2Block, ChartV2BlockDesigner, ChartV2BlockInitializer, chartIniti
import antd from './chart/antd';
import g2plot from './chart/g2plot';
import { ChartGroup } from './chart/group';
import { ChartRenderer, ChartRendererProvider } from './renderer';
import { lang } from './locale';
import { chartFilterActionInitializers, chartFilterItemInitializers } from './filter';
class DataVisualizationPlugin extends Plugin {
public charts: ChartGroup = new ChartGroup();
@ -12,17 +14,17 @@ class DataVisualizationPlugin extends Plugin {
this.app.addComponents({
ChartV2BlockInitializer,
ChartRenderer,
ChartV2BlockDesigner,
ChartV2Block,
ChartRendererProvider,
});
this.app.schemaInitializerManager.add(chartInitializers);
this.app.schemaInitializerManager.add(chartFilterItemInitializers);
this.app.schemaInitializerManager.add(chartFilterActionInitializers);
const blockInitializers = this.app.schemaInitializerManager.get('BlockInitializers');
blockInitializers?.add('dataBlocks.chartV2', {
title: '{{t("Charts")}}',
title: lang('Charts'),
Component: 'ChartV2BlockInitializer',
});
}
@ -31,6 +33,6 @@ class DataVisualizationPlugin extends Plugin {
export default DataVisualizationPlugin;
export { Chart } from './chart/chart';
export type { ChartProps, ChartType, RenderProps } from './chart/chart';
export { ChartConfigContext } from './configure/ChartConfigure';
export type { FieldOption } from './hooks';
export type { QueryProps } from './renderer';
export { ChartConfigContext } from './configure';

View File

@ -13,12 +13,13 @@ import {
import { Empty, Result, Spin, Typography } from 'antd';
import React, { useContext } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useChart } from '../chart/group';
import { ChartConfigContext } from '../configure/ChartConfigure';
import { ChartConfigContext } from '../configure';
import { useData, useFieldTransformer, useFieldsWithAssociation } from '../hooks';
import { useChartsTranslation } from '../locale';
import { createRendererSchema, getField } from '../utils';
import { ChartRendererContext } from './ChartRendererProvider';
import { useChart } from '../chart/group';
import { ChartDataContext } from '../block/ChartDataProvider';
const { Paragraph, Text } = Typography;
export const ChartRenderer: React.FC & {
@ -44,7 +45,7 @@ export const ChartRenderer: React.FC & {
if (!props[name]) {
const field = getField(fields, name.split('.'));
const transformer = transformers[name];
props[name] = { label: field?.label || name, transformer };
props[name] = { label: field?.label || name, transformer, interface: field?.interface };
}
return props;
}, {}),
@ -78,6 +79,7 @@ export const ChartRenderer: React.FC & {
ChartRenderer.Designer = function Designer() {
const { t } = useChartsTranslation();
const { setVisible, setCurrent } = useContext(ChartConfigContext);
const { removeChart } = useContext(ChartDataContext);
const { service } = useContext(ChartRendererContext);
const field = useField();
const schema = useFieldSchema();
@ -88,8 +90,8 @@ ChartRenderer.Designer = function Designer() {
<SchemaSettingsItem
title="Configure"
key="configure"
onClick={() => {
setCurrent({ schema, field, collection: name, service, data: service?.data });
onClick={async () => {
setCurrent({ schema, field, collection: name, service, data: service.data });
setVisible(true);
}}
>
@ -109,6 +111,11 @@ ChartRenderer.Designer = function Designer() {
breakRemoveOn={{
'x-component': 'ChartV2Block',
}}
confirm={{
onOk: () => {
removeChart(schema['x-uid']);
},
}}
/>
</GeneralSchemaDesigner>
);

View File

@ -1,8 +1,11 @@
import { css } from '@emotion/css';
import { useFieldSchema } from '@formily/react';
import { MaybeCollectionProvider, useAPIClient, useRequest } from '@nocobase/client';
import React, { createContext } from 'react';
import { parseField } from '../utils';
import React, { createContext, useContext } from 'react';
import { parseField, removeUnparsableFilter } from '../utils';
import { ChartDataContext } from '../block/ChartDataProvider';
import { ConfigProvider } from 'antd';
import { useChartFilter } from '../hooks';
import { ChartFilterContext } from '../filter/FilterProvider';
export type MeasureProps = {
field: string | string[];
@ -58,12 +61,23 @@ export const ChartRendererContext = createContext<
export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
const { query, config, collection, transform } = 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) =>
(collection, query, manual) =>
new Promise((resolve, reject) => {
// Check if the chart is configured
if (!(collection && query?.measures?.length)) return resolve(undefined);
// If the filter block is enabled, the filter form is required to be rendered
if (enabled && !form) return resolve(undefined);
const filterValues = getFilter();
const queryWithFilter =
!manual && hasFilter({ collection, query }, filterValues)
? appendFilter({ collection, query }, filterValues)
: query;
api
.request({
url: 'charts:query',
@ -71,7 +85,8 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
data: {
uid: schema?.['x-uid'],
collection,
...query,
...queryWithFilter,
filter: removeUnparsableFilter(queryWithFilter.filter),
dimensions: (query?.dimensions || []).map((item: DimensionProps) => {
const dimension = { ...item };
if (item.format && !item.alias) {
@ -92,27 +107,27 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
})
.then((res) => {
resolve(res?.data?.data);
if (!manual && schema?.['x-uid']) {
addChart(schema?.['x-uid'], { collection, service, query });
}
})
.catch(reject);
}),
{
defaultParams: [collection, query],
// Wait until CharrFilterProvider 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),
},
);
return (
<MaybeCollectionProvider collection={collection}>
<div
className={css`
.ant-card {
box-shadow: none;
}
`}
>
<ConfigProvider card={{ style: { boxShadow: 'none' } }}>
<ChartRendererContext.Provider value={{ collection, config, transform, service, query }}>
{props.children}
</ChartRendererContext.Provider>
</div>
</ConfigProvider>
</MaybeCollectionProvider>
);
};

View File

@ -1,8 +1,9 @@
import { Schema } from '@formily/react';
import { uid } from '@formily/shared';
import { SelectedField } from './configure/ChartConfigure';
import { SelectedField } from './configure';
import { FieldOption } from './hooks';
import { QueryProps } from './renderer';
import lodash from 'lodash';
export const createRendererSchema = (decoratorProps: any, componentProps = {}) => {
const { collection } = decoratorProps;
@ -100,3 +101,35 @@ export const processData = (selectedFields: FieldOption[], data: any[], scope: a
return processed;
});
};
export const removeUnparsableFilter = (filter: any) => {
if (typeof filter === 'object' && filter !== null) {
if (Array.isArray(filter)) {
const newLogic = filter.map((condition) => removeUnparsableFilter(condition)).filter(Boolean);
return newLogic.length > 0 ? newLogic : null;
} else {
const newLogic = {};
for (const key in filter) {
const value = removeUnparsableFilter(filter[key]);
if (value && !(typeof value === 'object' && Object.keys(value).length === 0)) {
newLogic[key] = value;
}
}
return Object.keys(newLogic).length > 0 ? newLogic : null;
}
} else if (typeof filter === 'string' && filter.startsWith('{{$nFilter.') && filter.endsWith('}}')) {
return null;
}
return filter;
};
export const getValuesByPath = (values: any, path: string) => {
const keys = path.split('.');
let result = lodash.get(values, keys.slice(0, -1).join('.'));
if (Array.isArray(result)) {
result = result.map((item) => lodash.get(item, keys.slice(-1)[0]));
} else {
result = lodash.get(result, keys.slice(-1)[0]);
}
return result;
};

View File

@ -20,4 +20,15 @@ export default {
'Are you sure you want to delete it?': 'Are you sure you want to delete it?',
'Sign in button name, which will be displayed on the sign in page':
'Sign in button name, which will be displayed on the sign in page',
Expand: 'Expand',
'Current filter': 'Current filter',
'Add custom field': 'Add custom field',
'To filter with custom fields, use "Current filter" variables in the chart configuration.':
'To filter with custom fields, use "Current filter" variables in the chart configuration.',
Input: 'Input',
'Date range': 'Date range',
'Time range': 'Time range',
'Edit field properties': 'Edit field properties',
'Select a source field to use metadata of the field': 'Select a source field to use metadata of the field',
'Original field': 'Original field',
};

View File

@ -73,4 +73,16 @@ export default {
isPercent: '显示为百分比',
isGroup: '分组',
smooth: '平滑曲线',
Collapse: '折叠',
Expand: '展开',
'Current filter': '当前筛选',
'Add custom field': '添加自定义字段',
'To filter with custom fields, use "Current filter" variables in the chart configuration.':
'要通过自定义字段筛选,需要在图表配置中使用“当前筛选”变量。',
Input: '输入框',
'Date range': '日期范围',
'Time range': '时间范围',
'Edit field properties': '编辑字段属性',
'Select a source field to use metadata of the field': '选择来源字段可以复用字段的元数据配置',
'Original field': '原始字段',
};