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:
parent
536001eaba
commit
16ad19b18d
@ -46,3 +46,4 @@ export * from './schema-templates';
|
|||||||
export * from './style';
|
export * from './style';
|
||||||
export * from './system-settings';
|
export * from './system-settings';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
export * from './variables';
|
||||||
|
@ -48,7 +48,7 @@ export function useAssociationFieldContext<F extends GeneralField>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function useServiceOptions(props) {
|
export default function useServiceOptions(props) {
|
||||||
const { action = 'list', service } = props;
|
const { action = 'list', service, useOriginalFilter } = props;
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const { getField } = useCollection();
|
const { getField } = useCollection();
|
||||||
@ -119,7 +119,7 @@ export default function useServiceOptions(props) {
|
|||||||
return mergeFilter(
|
return mergeFilter(
|
||||||
[
|
[
|
||||||
mergeFilter([
|
mergeFilter([
|
||||||
isOToAny && !isInFilterFormBlock(fieldSchema) && collectionField?.foreignKey
|
isOToAny && !isInFilterFormBlock(fieldSchema) && collectionField?.foreignKey && !useOriginalFilter
|
||||||
? {
|
? {
|
||||||
[collectionField.foreignKey]: {
|
[collectionField.foreignKey]: {
|
||||||
$is: null,
|
$is: null,
|
||||||
@ -132,7 +132,8 @@ export default function useServiceOptions(props) {
|
|||||||
sourceValue !== undefined &&
|
sourceValue !== undefined &&
|
||||||
sourceValue !== null &&
|
sourceValue !== null &&
|
||||||
!isInFilterFormBlock(fieldSchema) &&
|
!isInFilterFormBlock(fieldSchema) &&
|
||||||
collectionField?.foreignKey
|
collectionField?.foreignKey &&
|
||||||
|
!useOriginalFilter
|
||||||
? {
|
? {
|
||||||
[collectionField.foreignKey]: {
|
[collectionField.foreignKey]: {
|
||||||
$eq: sourceValue,
|
$eq: sourceValue,
|
||||||
@ -149,7 +150,14 @@ export default function useServiceOptions(props) {
|
|||||||
],
|
],
|
||||||
'$or',
|
'$or',
|
||||||
);
|
);
|
||||||
}, [collectionField?.interface, collectionField?.foreignKey, fieldSchema, fieldServiceFilter, sourceValue]);
|
}, [
|
||||||
|
collectionField?.interface,
|
||||||
|
collectionField?.foreignKey,
|
||||||
|
fieldSchema,
|
||||||
|
fieldServiceFilter,
|
||||||
|
sourceValue,
|
||||||
|
useOriginalFilter,
|
||||||
|
]);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return {
|
return {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from './FormItem';
|
export * from './FormItem';
|
||||||
export * from './FormItem.Settings';
|
export * from './FormItem.Settings';
|
||||||
export * from './FormItem.FilterFormSettings';
|
export * from './FormItem.FilterFormSettings';
|
||||||
|
export * from './SchemaSettingOptions';
|
||||||
|
@ -2,3 +2,4 @@ export * from './Input';
|
|||||||
export * from './ReadPretty';
|
export * from './ReadPretty';
|
||||||
export * from './Json';
|
export * from './Json';
|
||||||
export * from './EllipsisWithTooltip';
|
export * from './EllipsisWithTooltip';
|
||||||
|
export * from './shared';
|
||||||
|
@ -46,7 +46,7 @@ const InternalRemoteSelect = connect(
|
|||||||
const { getField } = useCollection();
|
const { getField } = useCollection();
|
||||||
const searchData = useRef(null);
|
const searchData = useRef(null);
|
||||||
const { getCollectionJoinField, getInterface } = useCollectionManager();
|
const { getCollectionJoinField, getInterface } = useCollectionManager();
|
||||||
const collectionField = getField(fieldSchema.name);
|
const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema.name as string);
|
||||||
const targetField =
|
const targetField =
|
||||||
_targetField ||
|
_targetField ||
|
||||||
(collectionField?.target &&
|
(collectionField?.target &&
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export * from './useBaseVariable';
|
||||||
|
export * from './useDateVariable';
|
||||||
|
export * from './useUserVariable';
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './hooks';
|
||||||
|
export * from './VariableInput';
|
@ -3,3 +3,5 @@ export * from './SchemaSettings';
|
|||||||
export * from './hooks/useIsAllowToSetDefaultValue';
|
export * from './hooks/useIsAllowToSetDefaultValue';
|
||||||
|
|
||||||
export { default as useParseDataScopeFilter } from './hooks/useParseDataScopeFilter';
|
export { default as useParseDataScopeFilter } from './hooks/useParseDataScopeFilter';
|
||||||
|
|
||||||
|
export * from './VariableInput';
|
||||||
|
@ -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 useBuiltinVariables } from './hooks/useBuiltinVariables';
|
||||||
export { default as useContextVariable } from './hooks/useContextVariable';
|
export { default as useContextVariable } from './hooks/useContextVariable';
|
||||||
export { default as useLocalVariables } from './hooks/useLocalVariables';
|
export { default as useLocalVariables } from './hooks/useLocalVariables';
|
||||||
|
@ -1,24 +1,27 @@
|
|||||||
import { SchemaInitializerContext, useDesignable, useSchemaInitializer } from '@nocobase/client';
|
import { SchemaComponentOptions, SchemaInitializerContext, useSchemaInitializer } from '@nocobase/client';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ChartConfigContext, ChartConfigCurrent, ChartConfigure } from '../configure/ChartConfigure';
|
import { ChartConfigProvider } from '../configure';
|
||||||
import { ChartRendererProvider } from '../renderer';
|
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) => {
|
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 [initialVisible, setInitialVisible] = useState(false);
|
||||||
const schemaInitializerContextData = useSchemaInitializer();
|
const schemaInitializerContextData = useSchemaInitializer();
|
||||||
return (
|
return (
|
||||||
<SchemaInitializerContext.Provider
|
<SchemaInitializerContext.Provider
|
||||||
value={{ ...schemaInitializerContextData, visible: initialVisible, setVisible: setInitialVisible }}
|
value={{ ...schemaInitializerContextData, visible: initialVisible, setVisible: setInitialVisible }}
|
||||||
>
|
>
|
||||||
<ChartConfigContext.Provider value={{ visible, setVisible, current, setCurrent }}>
|
<SchemaComponentOptions
|
||||||
{props.children}
|
components={{ ChartRenderer, ChartRendererProvider, ChartFilterBlockProvider, ChartFilterBlockDesigner }}
|
||||||
<ChartRendererProvider {...current.field?.decoratorProps}>
|
>
|
||||||
<ChartConfigure insert={(schema, options) => insertAdjacent('beforeEnd', schema, options)} />
|
<ChartDataProvider>
|
||||||
</ChartRendererProvider>
|
<ChartFilterProvider>
|
||||||
</ChartConfigContext.Provider>
|
<ChartConfigProvider>{props.children}</ChartConfigProvider>
|
||||||
|
</ChartFilterProvider>
|
||||||
|
</ChartDataProvider>
|
||||||
|
</SchemaComponentOptions>
|
||||||
</SchemaInitializerContext.Provider>
|
</SchemaInitializerContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,38 +1,26 @@
|
|||||||
import { LineChartOutlined } from '@ant-design/icons';
|
import { LineChartOutlined, BarChartOutlined } from '@ant-design/icons';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import {
|
import {
|
||||||
SchemaInitializerItem,
|
DataBlockInitializer,
|
||||||
SchemaInitializer,
|
SchemaInitializer,
|
||||||
|
SchemaInitializerItem,
|
||||||
useACLRoleContext,
|
useACLRoleContext,
|
||||||
useSchemaInitializerMenuItems,
|
|
||||||
useCollectionDataSourceItemsV2,
|
useCollectionDataSourceItemsV2,
|
||||||
useSchemaInitializer,
|
useSchemaInitializer,
|
||||||
useSchemaInitializerItem,
|
useSchemaInitializerItem,
|
||||||
useMenuSearch,
|
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { ChartConfigContext } from '../configure/ChartConfigure';
|
import { ChartConfigContext } from '../configure';
|
||||||
import { Menu } from 'antd';
|
import { lang } from '../locale';
|
||||||
|
import { FilterBlockInitializer } from '../filter';
|
||||||
|
|
||||||
const ConfigureButton = () => {
|
const ChartInitializer = () => {
|
||||||
const itemConfig = useSchemaInitializerItem();
|
|
||||||
const { setVisible, setCurrent } = useContext(ChartConfigContext);
|
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 collections = useCollectionDataSourceItemsV2('Chart');
|
||||||
const { allowAll, parseAction } = useACLRoleContext();
|
const { allowAll, parseAction } = useACLRoleContext();
|
||||||
const items: any = collections
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
|
||||||
|
const items = collections
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
if (allowAll) {
|
if (allowAll) {
|
||||||
return true;
|
return true;
|
||||||
@ -42,17 +30,17 @@ const ItemsComponent = () => {
|
|||||||
})
|
})
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
Component: ConfigureButton,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const [isOpenSubMenu, setIsOpenSubMenu] = useState(false);
|
|
||||||
const searchedChildren = useMenuSearch(items, isOpenSubMenu, true);
|
|
||||||
const menuItems = useSchemaInitializerMenuItems(searchedChildren);
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<DataBlockInitializer
|
||||||
items={menuItems}
|
{...itemConfig}
|
||||||
onOpenChange={(keys) => {
|
items={items}
|
||||||
setIsOpenSubMenu(keys.length > 0);
|
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({
|
export const chartInitializers = new SchemaInitializer({
|
||||||
name: 'ChartInitializers',
|
name: 'ChartInitializers',
|
||||||
icon: 'PlusOutlined',
|
icon: 'PlusOutlined',
|
||||||
popoverProps: {
|
title: '{{t("Add block")}}',
|
||||||
placement: 'bottomLeft',
|
items: [
|
||||||
},
|
{
|
||||||
title: '{{t("Add chart")}}',
|
name: 'chart',
|
||||||
ItemsComponent: ItemsComponent,
|
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 = () => {
|
export const ChartV2BlockInitializer: React.FC = () => {
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -15,9 +15,17 @@ export class Table extends AntdChart {
|
|||||||
key: item,
|
key: item,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
const rowKey = columns[0]?.dataIndex;
|
||||||
const dataSource = data.map((item: any) => {
|
const dataSource = data.map((item: any) => {
|
||||||
Object.keys(item).map((key: string) => {
|
Object.keys(item).map((key: string) => {
|
||||||
const props = fieldProps[key];
|
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) {
|
if (props?.transformer) {
|
||||||
item[key] = props.transformer(item[key]);
|
item[key] = props.transformer(item[key]);
|
||||||
}
|
}
|
||||||
@ -28,14 +36,18 @@ export class Table extends AntdChart {
|
|||||||
return {
|
return {
|
||||||
bordered: true,
|
bordered: true,
|
||||||
size: 'middle',
|
size: 'middle',
|
||||||
pagination:
|
// pagination:
|
||||||
dataSource.length < pageSize
|
// dataSource.length < pageSize
|
||||||
? false
|
// ? false
|
||||||
: {
|
// : {
|
||||||
pageSize,
|
// pageSize,
|
||||||
},
|
// },
|
||||||
dataSource,
|
dataSource,
|
||||||
columns,
|
columns,
|
||||||
|
scroll: {
|
||||||
|
x: 'max-content',
|
||||||
|
},
|
||||||
|
rowKey,
|
||||||
...general,
|
...general,
|
||||||
...advanced,
|
...advanced,
|
||||||
};
|
};
|
||||||
|
@ -14,6 +14,7 @@ export type RenderProps = {
|
|||||||
[field: string]: {
|
[field: string]: {
|
||||||
label: string;
|
label: string;
|
||||||
transformer: Transformer;
|
transformer: Transformer;
|
||||||
|
interface: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -13,7 +13,7 @@ import {
|
|||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography } from 'antd';
|
import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography } from 'antd';
|
||||||
import { cloneDeep, isEqual } from 'lodash';
|
import { cloneDeep, isEqual } from 'lodash';
|
||||||
import React, { createContext, useContext, useMemo, useRef } from 'react';
|
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
useChartFields,
|
useChartFields,
|
||||||
useCollectionOptions,
|
useCollectionOptions,
|
||||||
@ -31,32 +31,14 @@ import { createRendererSchema, getField, getSelectedFields } from '../utils';
|
|||||||
import { getConfigSchema, querySchema, transformSchema } from './schemas/configure';
|
import { getConfigSchema, querySchema, transformSchema } from './schemas/configure';
|
||||||
import { useChartTypes, useCharts, useDefaultChartType } from '../chart/group';
|
import { useChartTypes, useCharts, useDefaultChartType } from '../chart/group';
|
||||||
import { FilterDynamicComponent } from './FilterDynamicComponent';
|
import { FilterDynamicComponent } from './FilterDynamicComponent';
|
||||||
import { css } from '@emotion/css';
|
import { ChartConfigContext } from './ChartConfigProvider';
|
||||||
const { Paragraph, Text } = Typography;
|
const { Paragraph, Text } = Typography;
|
||||||
|
|
||||||
export type ChartConfigCurrent = {
|
|
||||||
schema: ISchema;
|
|
||||||
field: any;
|
|
||||||
collection: string;
|
|
||||||
service: any;
|
|
||||||
initialValues?: any;
|
|
||||||
data: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SelectedField = {
|
export type SelectedField = {
|
||||||
field: string | string[];
|
field: string | string[];
|
||||||
alias?: 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<{
|
export const ChartConfigure: React.FC<{
|
||||||
insert: (
|
insert: (
|
||||||
s: ISchema,
|
s: ISchema,
|
||||||
@ -151,7 +133,7 @@ export const ChartConfigure: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await service.runAsync(collection, form.values.query);
|
await service.runAsync(collection, form.values.query, true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
@ -164,6 +146,15 @@ export const ChartConfigure: React.FC<{
|
|||||||
|
|
||||||
const queryRef = useRef(null);
|
const queryRef = useRef(null);
|
||||||
const configRef = 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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t('Configure chart')}
|
title={t('Configure chart')}
|
||||||
@ -419,25 +410,19 @@ ChartConfigure.Data = function Data() {
|
|||||||
const data = useData(current?.data);
|
const data = useData(current?.data);
|
||||||
const error = service?.error;
|
const error = service?.error;
|
||||||
return !error ? (
|
return !error ? (
|
||||||
<div
|
<Table
|
||||||
style={{
|
dataSource={data}
|
||||||
overflowX: 'auto',
|
scroll={{ x: 'max-content' }}
|
||||||
overflowY: 'hidden',
|
columns={Object.keys(data[0] || {}).map((col) => {
|
||||||
}}
|
const field = getField(fields, col.split('.'));
|
||||||
>
|
return {
|
||||||
<Table
|
title: field?.label || col,
|
||||||
dataSource={data}
|
dataIndex: col,
|
||||||
columns={Object.keys(data[0] || {}).map((col) => {
|
key: col,
|
||||||
const field = getField(fields, col.split('.'));
|
};
|
||||||
return {
|
})}
|
||||||
title: field?.label || col,
|
size="small"
|
||||||
dataIndex: col,
|
/>
|
||||||
key: col,
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<Alert
|
<Alert
|
||||||
message="Error"
|
message="Error"
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './ChartConfigure';
|
||||||
|
export * from './ChartConfigProvider';
|
@ -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': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
});
|
@ -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} />;
|
||||||
|
});
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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)} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
export * from './FilterBlockDesigner';
|
||||||
|
export * from './FilterBlockInitializer';
|
||||||
|
export * from './FilterItemInitializers';
|
||||||
|
export * from './FilterActionInitializers';
|
||||||
|
export * from './FilterItemDesigner';
|
||||||
|
export * from './FilterBlockProvider';
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
@ -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]);
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
export * from './query';
|
export * from './query';
|
||||||
export * from './transformer';
|
export * from './transformer';
|
||||||
export * from './useVariableOptions';
|
export * from './useVariableOptions';
|
||||||
|
export * from './filter';
|
||||||
|
@ -3,7 +3,7 @@ import { ISchema, Schema, useForm } from '@formily/react';
|
|||||||
import { CollectionFieldOptions, useACLRoleContext, useCollectionManager } from '@nocobase/client';
|
import { CollectionFieldOptions, useACLRoleContext, useCollectionManager } from '@nocobase/client';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ChartConfigContext } from '../configure/ChartConfigure';
|
import { ChartConfigContext } from '../configure';
|
||||||
import formatters from '../block/formatters';
|
import formatters from '../block/formatters';
|
||||||
import { useChartsTranslation } from '../locale';
|
import { useChartsTranslation } from '../locale';
|
||||||
import { ChartRendererContext } from '../renderer';
|
import { ChartRendererContext } from '../renderer';
|
||||||
|
@ -2,19 +2,19 @@ import { useMemo } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
|
||||||
export const useDateVariable = ({ operator, schema }) => {
|
export const useDateVariable = ({ schema }) => {
|
||||||
const { t: trans } = useTranslation();
|
const { t: trans } = useTranslation();
|
||||||
const t = useMemoizedFn(trans);
|
const t = useMemoizedFn(trans);
|
||||||
const operatorValue = operator?.value || '';
|
|
||||||
const component = schema?.['x-component'];
|
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(
|
const dateOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
key: 'now',
|
key: 'now',
|
||||||
value: 'now',
|
value: 'now',
|
||||||
label: t('Now'),
|
label: t('Now'),
|
||||||
disabled: component !== 'DatePicker' || operatorValue === '$dateBetween',
|
disabled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'yesterday',
|
key: 'yesterday',
|
||||||
@ -143,7 +143,7 @@ export const useDateVariable = ({ operator, schema }) => {
|
|||||||
disabled,
|
disabled,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[disabled, operatorValue, t, component],
|
[disabled, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = useMemo(
|
const result = useMemo(
|
||||||
@ -157,7 +157,7 @@ export const useDateVariable = ({ operator, schema }) => {
|
|||||||
[dateOptions, t],
|
[dateOptions, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!operator || !schema) return null;
|
if (!schema) return null;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,7 @@ export const useUserVariable = ({ schema }: { schema: any }) => {
|
|||||||
const result = {
|
const result = {
|
||||||
key: option.name,
|
key: option.name,
|
||||||
value: 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 }),
|
label: Schema.compile(option.title, { t }),
|
||||||
};
|
};
|
||||||
if (option.children) {
|
if (option.children) {
|
||||||
@ -23,7 +23,7 @@ export const useUserVariable = ({ schema }: { schema: any }) => {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
[t, component],
|
[t],
|
||||||
);
|
);
|
||||||
const children = useMemo(() => getOptions(options), [getOptions, options]);
|
const children = useMemo(() => getOptions(options), [getOptions, options]);
|
||||||
const result = useMemo(
|
const result = useMemo(
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
import { useField } from '@formily/react';
|
import { useField } from '@formily/react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useDateVariable } from './useDateVariable';
|
import { useFilterVariable } from './filter';
|
||||||
import { useUserVariable } from './useUserVariable';
|
import { useDateVariable, useUserVariable } from '@nocobase/client';
|
||||||
|
|
||||||
export const useVariableOptions = () => {
|
export const useVariableOptions = () => {
|
||||||
const field = useField<any>();
|
const field = useField<any>();
|
||||||
const { operator, schema } = field.data || {};
|
const { operator, schema } = field.data || {};
|
||||||
const userVariable = useUserVariable({ schema });
|
const userVariable = useUserVariable({
|
||||||
|
collectionField: { uiSchema: schema },
|
||||||
|
uiSchema: schema,
|
||||||
|
});
|
||||||
const dateVariable = useDateVariable({ operator, 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 [];
|
if (!operator || !schema) return [];
|
||||||
|
|
||||||
|
@ -3,7 +3,9 @@ import { ChartV2Block, ChartV2BlockDesigner, ChartV2BlockInitializer, chartIniti
|
|||||||
import antd from './chart/antd';
|
import antd from './chart/antd';
|
||||||
import g2plot from './chart/g2plot';
|
import g2plot from './chart/g2plot';
|
||||||
import { ChartGroup } from './chart/group';
|
import { ChartGroup } from './chart/group';
|
||||||
import { ChartRenderer, ChartRendererProvider } from './renderer';
|
import { lang } from './locale';
|
||||||
|
import { chartFilterActionInitializers, chartFilterItemInitializers } from './filter';
|
||||||
|
|
||||||
class DataVisualizationPlugin extends Plugin {
|
class DataVisualizationPlugin extends Plugin {
|
||||||
public charts: ChartGroup = new ChartGroup();
|
public charts: ChartGroup = new ChartGroup();
|
||||||
|
|
||||||
@ -12,17 +14,17 @@ class DataVisualizationPlugin extends Plugin {
|
|||||||
|
|
||||||
this.app.addComponents({
|
this.app.addComponents({
|
||||||
ChartV2BlockInitializer,
|
ChartV2BlockInitializer,
|
||||||
ChartRenderer,
|
|
||||||
ChartV2BlockDesigner,
|
ChartV2BlockDesigner,
|
||||||
ChartV2Block,
|
ChartV2Block,
|
||||||
ChartRendererProvider,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.app.schemaInitializerManager.add(chartInitializers);
|
this.app.schemaInitializerManager.add(chartInitializers);
|
||||||
|
this.app.schemaInitializerManager.add(chartFilterItemInitializers);
|
||||||
|
this.app.schemaInitializerManager.add(chartFilterActionInitializers);
|
||||||
|
|
||||||
const blockInitializers = this.app.schemaInitializerManager.get('BlockInitializers');
|
const blockInitializers = this.app.schemaInitializerManager.get('BlockInitializers');
|
||||||
blockInitializers?.add('dataBlocks.chartV2', {
|
blockInitializers?.add('dataBlocks.chartV2', {
|
||||||
title: '{{t("Charts")}}',
|
title: lang('Charts'),
|
||||||
Component: 'ChartV2BlockInitializer',
|
Component: 'ChartV2BlockInitializer',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -31,6 +33,6 @@ class DataVisualizationPlugin extends Plugin {
|
|||||||
export default DataVisualizationPlugin;
|
export default DataVisualizationPlugin;
|
||||||
export { Chart } from './chart/chart';
|
export { Chart } from './chart/chart';
|
||||||
export type { ChartProps, ChartType, RenderProps } from './chart/chart';
|
export type { ChartProps, ChartType, RenderProps } from './chart/chart';
|
||||||
export { ChartConfigContext } from './configure/ChartConfigure';
|
|
||||||
export type { FieldOption } from './hooks';
|
export type { FieldOption } from './hooks';
|
||||||
export type { QueryProps } from './renderer';
|
export type { QueryProps } from './renderer';
|
||||||
|
export { ChartConfigContext } from './configure';
|
||||||
|
@ -13,12 +13,13 @@ import {
|
|||||||
import { Empty, Result, Spin, Typography } from 'antd';
|
import { Empty, Result, Spin, Typography } from 'antd';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useChart } from '../chart/group';
|
import { ChartConfigContext } from '../configure';
|
||||||
import { ChartConfigContext } from '../configure/ChartConfigure';
|
|
||||||
import { useData, useFieldTransformer, useFieldsWithAssociation } from '../hooks';
|
import { useData, useFieldTransformer, useFieldsWithAssociation } from '../hooks';
|
||||||
import { useChartsTranslation } from '../locale';
|
import { useChartsTranslation } from '../locale';
|
||||||
import { createRendererSchema, getField } from '../utils';
|
import { createRendererSchema, getField } from '../utils';
|
||||||
import { ChartRendererContext } from './ChartRendererProvider';
|
import { ChartRendererContext } from './ChartRendererProvider';
|
||||||
|
import { useChart } from '../chart/group';
|
||||||
|
import { ChartDataContext } from '../block/ChartDataProvider';
|
||||||
const { Paragraph, Text } = Typography;
|
const { Paragraph, Text } = Typography;
|
||||||
|
|
||||||
export const ChartRenderer: React.FC & {
|
export const ChartRenderer: React.FC & {
|
||||||
@ -44,7 +45,7 @@ export const ChartRenderer: React.FC & {
|
|||||||
if (!props[name]) {
|
if (!props[name]) {
|
||||||
const field = getField(fields, name.split('.'));
|
const field = getField(fields, name.split('.'));
|
||||||
const transformer = transformers[name];
|
const transformer = transformers[name];
|
||||||
props[name] = { label: field?.label || name, transformer };
|
props[name] = { label: field?.label || name, transformer, interface: field?.interface };
|
||||||
}
|
}
|
||||||
return props;
|
return props;
|
||||||
}, {}),
|
}, {}),
|
||||||
@ -78,6 +79,7 @@ export const ChartRenderer: React.FC & {
|
|||||||
ChartRenderer.Designer = function Designer() {
|
ChartRenderer.Designer = function Designer() {
|
||||||
const { t } = useChartsTranslation();
|
const { t } = useChartsTranslation();
|
||||||
const { setVisible, setCurrent } = useContext(ChartConfigContext);
|
const { setVisible, setCurrent } = useContext(ChartConfigContext);
|
||||||
|
const { removeChart } = useContext(ChartDataContext);
|
||||||
const { service } = useContext(ChartRendererContext);
|
const { service } = useContext(ChartRendererContext);
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
@ -88,8 +90,8 @@ ChartRenderer.Designer = function Designer() {
|
|||||||
<SchemaSettingsItem
|
<SchemaSettingsItem
|
||||||
title="Configure"
|
title="Configure"
|
||||||
key="configure"
|
key="configure"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
setCurrent({ schema, field, collection: name, service, data: service?.data });
|
setCurrent({ schema, field, collection: name, service, data: service.data });
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -109,6 +111,11 @@ ChartRenderer.Designer = function Designer() {
|
|||||||
breakRemoveOn={{
|
breakRemoveOn={{
|
||||||
'x-component': 'ChartV2Block',
|
'x-component': 'ChartV2Block',
|
||||||
}}
|
}}
|
||||||
|
confirm={{
|
||||||
|
onOk: () => {
|
||||||
|
removeChart(schema['x-uid']);
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</GeneralSchemaDesigner>
|
</GeneralSchemaDesigner>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { MaybeCollectionProvider, useAPIClient, useRequest } from '@nocobase/client';
|
import { MaybeCollectionProvider, useAPIClient, useRequest } from '@nocobase/client';
|
||||||
import React, { createContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import { parseField } from '../utils';
|
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 = {
|
export type MeasureProps = {
|
||||||
field: string | string[];
|
field: string | string[];
|
||||||
@ -58,12 +61,23 @@ export const ChartRendererContext = createContext<
|
|||||||
|
|
||||||
export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
|
export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
|
||||||
const { query, config, collection, transform } = 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 schema = useFieldSchema();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const service = useRequest(
|
const service = useRequest(
|
||||||
(collection, query) =>
|
(collection, query, manual) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
|
// Check if the chart is configured
|
||||||
if (!(collection && query?.measures?.length)) return resolve(undefined);
|
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
|
api
|
||||||
.request({
|
.request({
|
||||||
url: 'charts:query',
|
url: 'charts:query',
|
||||||
@ -71,7 +85,8 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
|
|||||||
data: {
|
data: {
|
||||||
uid: schema?.['x-uid'],
|
uid: schema?.['x-uid'],
|
||||||
collection,
|
collection,
|
||||||
...query,
|
...queryWithFilter,
|
||||||
|
filter: removeUnparsableFilter(queryWithFilter.filter),
|
||||||
dimensions: (query?.dimensions || []).map((item: DimensionProps) => {
|
dimensions: (query?.dimensions || []).map((item: DimensionProps) => {
|
||||||
const dimension = { ...item };
|
const dimension = { ...item };
|
||||||
if (item.format && !item.alias) {
|
if (item.format && !item.alias) {
|
||||||
@ -92,27 +107,27 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
|
|||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
resolve(res?.data?.data);
|
resolve(res?.data?.data);
|
||||||
|
if (!manual && schema?.['x-uid']) {
|
||||||
|
addChart(schema?.['x-uid'], { collection, service, query });
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
defaultParams: [collection, query],
|
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 (
|
return (
|
||||||
<MaybeCollectionProvider collection={collection}>
|
<MaybeCollectionProvider collection={collection}>
|
||||||
<div
|
<ConfigProvider card={{ style: { boxShadow: 'none' } }}>
|
||||||
className={css`
|
|
||||||
.ant-card {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<ChartRendererContext.Provider value={{ collection, config, transform, service, query }}>
|
<ChartRendererContext.Provider value={{ collection, config, transform, service, query }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</ChartRendererContext.Provider>
|
</ChartRendererContext.Provider>
|
||||||
</div>
|
</ConfigProvider>
|
||||||
</MaybeCollectionProvider>
|
</MaybeCollectionProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Schema } from '@formily/react';
|
import { Schema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { SelectedField } from './configure/ChartConfigure';
|
import { SelectedField } from './configure';
|
||||||
import { FieldOption } from './hooks';
|
import { FieldOption } from './hooks';
|
||||||
import { QueryProps } from './renderer';
|
import { QueryProps } from './renderer';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
export const createRendererSchema = (decoratorProps: any, componentProps = {}) => {
|
export const createRendererSchema = (decoratorProps: any, componentProps = {}) => {
|
||||||
const { collection } = decoratorProps;
|
const { collection } = decoratorProps;
|
||||||
@ -100,3 +101,35 @@ export const processData = (selectedFields: FieldOption[], data: any[], scope: a
|
|||||||
return processed;
|
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;
|
||||||
|
};
|
||||||
|
@ -20,4 +20,15 @@ export default {
|
|||||||
'Are you sure you want to delete it?': 'Are you sure you want to delete it?',
|
'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':
|
||||||
'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',
|
||||||
};
|
};
|
||||||
|
@ -73,4 +73,16 @@ export default {
|
|||||||
isPercent: '显示为百分比',
|
isPercent: '显示为百分比',
|
||||||
isGroup: '分组',
|
isGroup: '分组',
|
||||||
smooth: '平滑曲线',
|
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': '原始字段',
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user