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 './system-settings';
|
||||
export * from './user';
|
||||
export * from './variables';
|
||||
|
@ -48,7 +48,7 @@ export function useAssociationFieldContext<F extends GeneralField>() {
|
||||
}
|
||||
|
||||
export default function useServiceOptions(props) {
|
||||
const { action = 'list', service } = props;
|
||||
const { action = 'list', service, useOriginalFilter } = props;
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField();
|
||||
const { getField } = useCollection();
|
||||
@ -119,7 +119,7 @@ export default function useServiceOptions(props) {
|
||||
return mergeFilter(
|
||||
[
|
||||
mergeFilter([
|
||||
isOToAny && !isInFilterFormBlock(fieldSchema) && collectionField?.foreignKey
|
||||
isOToAny && !isInFilterFormBlock(fieldSchema) && collectionField?.foreignKey && !useOriginalFilter
|
||||
? {
|
||||
[collectionField.foreignKey]: {
|
||||
$is: null,
|
||||
@ -132,7 +132,8 @@ export default function useServiceOptions(props) {
|
||||
sourceValue !== undefined &&
|
||||
sourceValue !== null &&
|
||||
!isInFilterFormBlock(fieldSchema) &&
|
||||
collectionField?.foreignKey
|
||||
collectionField?.foreignKey &&
|
||||
!useOriginalFilter
|
||||
? {
|
||||
[collectionField.foreignKey]: {
|
||||
$eq: sourceValue,
|
||||
@ -149,7 +150,14 @@ export default function useServiceOptions(props) {
|
||||
],
|
||||
'$or',
|
||||
);
|
||||
}, [collectionField?.interface, collectionField?.foreignKey, fieldSchema, fieldServiceFilter, sourceValue]);
|
||||
}, [
|
||||
collectionField?.interface,
|
||||
collectionField?.foreignKey,
|
||||
fieldSchema,
|
||||
fieldServiceFilter,
|
||||
sourceValue,
|
||||
useOriginalFilter,
|
||||
]);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from './FormItem';
|
||||
export * from './FormItem.Settings';
|
||||
export * from './FormItem.FilterFormSettings';
|
||||
export * from './SchemaSettingOptions';
|
||||
|
@ -2,3 +2,4 @@ export * from './Input';
|
||||
export * from './ReadPretty';
|
||||
export * from './Json';
|
||||
export * from './EllipsisWithTooltip';
|
||||
export * from './shared';
|
||||
|
@ -46,7 +46,7 @@ const InternalRemoteSelect = connect(
|
||||
const { getField } = useCollection();
|
||||
const searchData = useRef(null);
|
||||
const { getCollectionJoinField, getInterface } = useCollectionManager();
|
||||
const collectionField = getField(fieldSchema.name);
|
||||
const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema.name as string);
|
||||
const targetField =
|
||||
_targetField ||
|
||||
(collectionField?.target &&
|
||||
|
@ -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 { 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 useContextVariable } from './hooks/useContextVariable';
|
||||
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 { ChartConfigContext, ChartConfigCurrent, ChartConfigure } from '../configure/ChartConfigure';
|
||||
import { ChartRendererProvider } from '../renderer';
|
||||
import { ChartConfigProvider } from '../configure';
|
||||
import { ChartDataProvider } from './ChartDataProvider';
|
||||
import { ChartRenderer, ChartRendererProvider } from '../renderer';
|
||||
import { ChartFilterBlockProvider, ChartFilterBlockDesigner } from '../filter';
|
||||
import { ChartFilterProvider } from '../filter/FilterProvider';
|
||||
|
||||
export const ChartV2Block: React.FC = (props) => {
|
||||
const { insertAdjacent } = useDesignable();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [current, setCurrent] = useState<ChartConfigCurrent>({} as any);
|
||||
const [initialVisible, setInitialVisible] = useState(false);
|
||||
const schemaInitializerContextData = useSchemaInitializer();
|
||||
return (
|
||||
<SchemaInitializerContext.Provider
|
||||
value={{ ...schemaInitializerContextData, visible: initialVisible, setVisible: setInitialVisible }}
|
||||
>
|
||||
<ChartConfigContext.Provider value={{ visible, setVisible, current, setCurrent }}>
|
||||
{props.children}
|
||||
<ChartRendererProvider {...current.field?.decoratorProps}>
|
||||
<ChartConfigure insert={(schema, options) => insertAdjacent('beforeEnd', schema, options)} />
|
||||
</ChartRendererProvider>
|
||||
</ChartConfigContext.Provider>
|
||||
<SchemaComponentOptions
|
||||
components={{ ChartRenderer, ChartRendererProvider, ChartFilterBlockProvider, ChartFilterBlockDesigner }}
|
||||
>
|
||||
<ChartDataProvider>
|
||||
<ChartFilterProvider>
|
||||
<ChartConfigProvider>{props.children}</ChartConfigProvider>
|
||||
</ChartFilterProvider>
|
||||
</ChartDataProvider>
|
||||
</SchemaComponentOptions>
|
||||
</SchemaInitializerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -1,38 +1,26 @@
|
||||
import { LineChartOutlined } from '@ant-design/icons';
|
||||
import { LineChartOutlined, BarChartOutlined } from '@ant-design/icons';
|
||||
import { uid } from '@formily/shared';
|
||||
import {
|
||||
SchemaInitializerItem,
|
||||
DataBlockInitializer,
|
||||
SchemaInitializer,
|
||||
SchemaInitializerItem,
|
||||
useACLRoleContext,
|
||||
useSchemaInitializerMenuItems,
|
||||
useCollectionDataSourceItemsV2,
|
||||
useSchemaInitializer,
|
||||
useSchemaInitializerItem,
|
||||
useMenuSearch,
|
||||
} from '@nocobase/client';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { ChartConfigContext } from '../configure/ChartConfigure';
|
||||
import { Menu } from 'antd';
|
||||
import React, { useContext } from 'react';
|
||||
import { ChartConfigContext } from '../configure';
|
||||
import { lang } from '../locale';
|
||||
import { FilterBlockInitializer } from '../filter';
|
||||
|
||||
const ConfigureButton = () => {
|
||||
const itemConfig = useSchemaInitializerItem();
|
||||
const ChartInitializer = () => {
|
||||
const { setVisible, setCurrent } = useContext(ChartConfigContext);
|
||||
return (
|
||||
<SchemaInitializerItem
|
||||
{...itemConfig}
|
||||
applyMenuStyle={false}
|
||||
onClick={() => {
|
||||
setCurrent({ schema: {}, field: null, collection: itemConfig?.name, service: null, data: undefined });
|
||||
setVisible(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ItemsComponent = () => {
|
||||
const collections = useCollectionDataSourceItemsV2('Chart');
|
||||
const { allowAll, parseAction } = useACLRoleContext();
|
||||
const items: any = collections
|
||||
const itemConfig = useSchemaInitializerItem();
|
||||
|
||||
const items = collections
|
||||
.filter((item) => {
|
||||
if (allowAll) {
|
||||
return true;
|
||||
@ -42,17 +30,17 @@ const ItemsComponent = () => {
|
||||
})
|
||||
.map((item) => ({
|
||||
...item,
|
||||
Component: ConfigureButton,
|
||||
}));
|
||||
|
||||
const [isOpenSubMenu, setIsOpenSubMenu] = useState(false);
|
||||
const searchedChildren = useMenuSearch(items, isOpenSubMenu, true);
|
||||
const menuItems = useSchemaInitializerMenuItems(searchedChildren);
|
||||
return (
|
||||
<Menu
|
||||
items={menuItems}
|
||||
onOpenChange={(keys) => {
|
||||
setIsOpenSubMenu(keys.length > 0);
|
||||
<DataBlockInitializer
|
||||
{...itemConfig}
|
||||
items={items}
|
||||
icon={<BarChartOutlined />}
|
||||
componentType={'Chart'}
|
||||
onCreateBlockSchema={async ({ item }) => {
|
||||
setCurrent({ schema: {}, field: null, collection: item.name, service: null, data: undefined });
|
||||
setVisible(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -61,11 +49,26 @@ const ItemsComponent = () => {
|
||||
export const chartInitializers = new SchemaInitializer({
|
||||
name: 'ChartInitializers',
|
||||
icon: 'PlusOutlined',
|
||||
popoverProps: {
|
||||
placement: 'bottomLeft',
|
||||
},
|
||||
title: '{{t("Add chart")}}',
|
||||
ItemsComponent: ItemsComponent,
|
||||
title: '{{t("Add block")}}',
|
||||
items: [
|
||||
{
|
||||
name: 'chart',
|
||||
title: lang('Chart'),
|
||||
Component: ChartInitializer,
|
||||
},
|
||||
{
|
||||
name: 'otherBlocks',
|
||||
type: 'itemGroup',
|
||||
title: lang('Other blocks'),
|
||||
children: [
|
||||
{
|
||||
name: 'filter',
|
||||
title: lang('Filter'),
|
||||
Component: FilterBlockInitializer,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const ChartV2BlockInitializer: React.FC = () => {
|
||||
|
@ -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,
|
||||
}))
|
||||
: [];
|
||||
const rowKey = columns[0]?.dataIndex;
|
||||
const dataSource = data.map((item: any) => {
|
||||
Object.keys(item).map((key: string) => {
|
||||
const props = fieldProps[key];
|
||||
if (props?.interface === 'percent') {
|
||||
const value = Math.round(parseFloat(item[key]) * 100).toFixed(2);
|
||||
item[key] = `${value}%`;
|
||||
}
|
||||
if (typeof item[key] === 'boolean') {
|
||||
item[key] = item[key].toString();
|
||||
}
|
||||
if (props?.transformer) {
|
||||
item[key] = props.transformer(item[key]);
|
||||
}
|
||||
@ -28,14 +36,18 @@ export class Table extends AntdChart {
|
||||
return {
|
||||
bordered: true,
|
||||
size: 'middle',
|
||||
pagination:
|
||||
dataSource.length < pageSize
|
||||
? false
|
||||
: {
|
||||
pageSize,
|
||||
},
|
||||
// pagination:
|
||||
// dataSource.length < pageSize
|
||||
// ? false
|
||||
// : {
|
||||
// pageSize,
|
||||
// },
|
||||
dataSource,
|
||||
columns,
|
||||
scroll: {
|
||||
x: 'max-content',
|
||||
},
|
||||
rowKey,
|
||||
...general,
|
||||
...advanced,
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ export type RenderProps = {
|
||||
[field: string]: {
|
||||
label: string;
|
||||
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';
|
||||
import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography } from 'antd';
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import React, { createContext, useContext, useMemo, useRef } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
||||
import {
|
||||
useChartFields,
|
||||
useCollectionOptions,
|
||||
@ -31,32 +31,14 @@ import { createRendererSchema, getField, getSelectedFields } from '../utils';
|
||||
import { getConfigSchema, querySchema, transformSchema } from './schemas/configure';
|
||||
import { useChartTypes, useCharts, useDefaultChartType } from '../chart/group';
|
||||
import { FilterDynamicComponent } from './FilterDynamicComponent';
|
||||
import { css } from '@emotion/css';
|
||||
import { ChartConfigContext } from './ChartConfigProvider';
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
export type ChartConfigCurrent = {
|
||||
schema: ISchema;
|
||||
field: any;
|
||||
collection: string;
|
||||
service: any;
|
||||
initialValues?: any;
|
||||
data: any[];
|
||||
};
|
||||
|
||||
export type SelectedField = {
|
||||
field: string | string[];
|
||||
alias?: string;
|
||||
};
|
||||
|
||||
export const ChartConfigContext = createContext<{
|
||||
visible: boolean;
|
||||
setVisible?: (visible: boolean) => void;
|
||||
current?: ChartConfigCurrent;
|
||||
setCurrent?: (current: ChartConfigCurrent) => void;
|
||||
}>({
|
||||
visible: true,
|
||||
});
|
||||
|
||||
export const ChartConfigure: React.FC<{
|
||||
insert: (
|
||||
s: ISchema,
|
||||
@ -151,7 +133,7 @@ export const ChartConfigure: React.FC<{
|
||||
}
|
||||
|
||||
try {
|
||||
await service.runAsync(collection, form.values.query);
|
||||
await service.runAsync(collection, form.values.query, true);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
@ -164,6 +146,15 @@ export const ChartConfigure: React.FC<{
|
||||
|
||||
const queryRef = useRef(null);
|
||||
const configRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
service.run(collection, field?.decoratorProps?.query, 'configure');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('Configure chart')}
|
||||
@ -419,25 +410,19 @@ ChartConfigure.Data = function Data() {
|
||||
const data = useData(current?.data);
|
||||
const error = service?.error;
|
||||
return !error ? (
|
||||
<div
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
dataSource={data}
|
||||
columns={Object.keys(data[0] || {}).map((col) => {
|
||||
const field = getField(fields, col.split('.'));
|
||||
return {
|
||||
title: field?.label || col,
|
||||
dataIndex: col,
|
||||
key: col,
|
||||
};
|
||||
})}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
dataSource={data}
|
||||
scroll={{ x: 'max-content' }}
|
||||
columns={Object.keys(data[0] || {}).map((col) => {
|
||||
const field = getField(fields, col.split('.'));
|
||||
return {
|
||||
title: field?.label || col,
|
||||
dataIndex: col,
|
||||
key: col,
|
||||
};
|
||||
})}
|
||||
size="small"
|
||||
/>
|
||||
) : (
|
||||
<Alert
|
||||
message="Error"
|
||||
|
@ -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 './transformer';
|
||||
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 { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChartConfigContext } from '../configure/ChartConfigure';
|
||||
import { ChartConfigContext } from '../configure';
|
||||
import formatters from '../block/formatters';
|
||||
import { useChartsTranslation } from '../locale';
|
||||
import { ChartRendererContext } from '../renderer';
|
||||
|
@ -2,19 +2,19 @@ import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
export const useDateVariable = ({ operator, schema }) => {
|
||||
export const useDateVariable = ({ schema }) => {
|
||||
const { t: trans } = useTranslation();
|
||||
const t = useMemoizedFn(trans);
|
||||
const operatorValue = operator?.value || '';
|
||||
const component = schema?.['x-component'];
|
||||
const disabled = !['DatePicker', 'DatePicker.RangePicker'].includes(component);
|
||||
// const disabled = !['DatePicker', 'DatePicker.RangePicker'].includes(component);
|
||||
const disabled = false;
|
||||
const dateOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'now',
|
||||
value: 'now',
|
||||
label: t('Now'),
|
||||
disabled: component !== 'DatePicker' || operatorValue === '$dateBetween',
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'yesterday',
|
||||
@ -143,7 +143,7 @@ export const useDateVariable = ({ operator, schema }) => {
|
||||
disabled,
|
||||
},
|
||||
],
|
||||
[disabled, operatorValue, t, component],
|
||||
[disabled, t],
|
||||
);
|
||||
|
||||
const result = useMemo(
|
||||
@ -157,7 +157,7 @@ export const useDateVariable = ({ operator, schema }) => {
|
||||
[dateOptions, t],
|
||||
);
|
||||
|
||||
if (!operator || !schema) return null;
|
||||
if (!schema) return null;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ export const useUserVariable = ({ schema }: { schema: any }) => {
|
||||
const result = {
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
disabled: !option.children && component !== option.schema?.['x-component'],
|
||||
// disabled: !option.children && component !== option.schema?.['x-component'],
|
||||
label: Schema.compile(option.title, { t }),
|
||||
};
|
||||
if (option.children) {
|
||||
@ -23,7 +23,7 @@ export const useUserVariable = ({ schema }: { schema: any }) => {
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
[t, component],
|
||||
[t],
|
||||
);
|
||||
const children = useMemo(() => getOptions(options), [getOptions, options]);
|
||||
const result = useMemo(
|
||||
|
@ -1,15 +1,22 @@
|
||||
import { useField } from '@formily/react';
|
||||
import { useMemo } from 'react';
|
||||
import { useDateVariable } from './useDateVariable';
|
||||
import { useUserVariable } from './useUserVariable';
|
||||
import { useFilterVariable } from './filter';
|
||||
import { useDateVariable, useUserVariable } from '@nocobase/client';
|
||||
|
||||
export const useVariableOptions = () => {
|
||||
const field = useField<any>();
|
||||
const { operator, schema } = field.data || {};
|
||||
const userVariable = useUserVariable({ schema });
|
||||
const userVariable = useUserVariable({
|
||||
collectionField: { uiSchema: schema },
|
||||
uiSchema: schema,
|
||||
});
|
||||
const dateVariable = useDateVariable({ operator, schema });
|
||||
const filterVariable = useFilterVariable();
|
||||
|
||||
const result = useMemo(() => [userVariable, dateVariable].filter(Boolean), [dateVariable, userVariable]);
|
||||
const result = useMemo(
|
||||
() => [userVariable, dateVariable, filterVariable].filter(Boolean),
|
||||
[dateVariable, userVariable, filterVariable],
|
||||
);
|
||||
|
||||
if (!operator || !schema) return [];
|
||||
|
||||
|
@ -3,7 +3,9 @@ import { ChartV2Block, ChartV2BlockDesigner, ChartV2BlockInitializer, chartIniti
|
||||
import antd from './chart/antd';
|
||||
import g2plot from './chart/g2plot';
|
||||
import { ChartGroup } from './chart/group';
|
||||
import { ChartRenderer, ChartRendererProvider } from './renderer';
|
||||
import { lang } from './locale';
|
||||
import { chartFilterActionInitializers, chartFilterItemInitializers } from './filter';
|
||||
|
||||
class DataVisualizationPlugin extends Plugin {
|
||||
public charts: ChartGroup = new ChartGroup();
|
||||
|
||||
@ -12,17 +14,17 @@ class DataVisualizationPlugin extends Plugin {
|
||||
|
||||
this.app.addComponents({
|
||||
ChartV2BlockInitializer,
|
||||
ChartRenderer,
|
||||
ChartV2BlockDesigner,
|
||||
ChartV2Block,
|
||||
ChartRendererProvider,
|
||||
});
|
||||
|
||||
this.app.schemaInitializerManager.add(chartInitializers);
|
||||
this.app.schemaInitializerManager.add(chartFilterItemInitializers);
|
||||
this.app.schemaInitializerManager.add(chartFilterActionInitializers);
|
||||
|
||||
const blockInitializers = this.app.schemaInitializerManager.get('BlockInitializers');
|
||||
blockInitializers?.add('dataBlocks.chartV2', {
|
||||
title: '{{t("Charts")}}',
|
||||
title: lang('Charts'),
|
||||
Component: 'ChartV2BlockInitializer',
|
||||
});
|
||||
}
|
||||
@ -31,6 +33,6 @@ class DataVisualizationPlugin extends Plugin {
|
||||
export default DataVisualizationPlugin;
|
||||
export { Chart } from './chart/chart';
|
||||
export type { ChartProps, ChartType, RenderProps } from './chart/chart';
|
||||
export { ChartConfigContext } from './configure/ChartConfigure';
|
||||
export type { FieldOption } from './hooks';
|
||||
export type { QueryProps } from './renderer';
|
||||
export { ChartConfigContext } from './configure';
|
||||
|
@ -13,12 +13,13 @@ import {
|
||||
import { Empty, Result, Spin, Typography } from 'antd';
|
||||
import React, { useContext } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useChart } from '../chart/group';
|
||||
import { ChartConfigContext } from '../configure/ChartConfigure';
|
||||
import { ChartConfigContext } from '../configure';
|
||||
import { useData, useFieldTransformer, useFieldsWithAssociation } from '../hooks';
|
||||
import { useChartsTranslation } from '../locale';
|
||||
import { createRendererSchema, getField } from '../utils';
|
||||
import { ChartRendererContext } from './ChartRendererProvider';
|
||||
import { useChart } from '../chart/group';
|
||||
import { ChartDataContext } from '../block/ChartDataProvider';
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
export const ChartRenderer: React.FC & {
|
||||
@ -44,7 +45,7 @@ export const ChartRenderer: React.FC & {
|
||||
if (!props[name]) {
|
||||
const field = getField(fields, name.split('.'));
|
||||
const transformer = transformers[name];
|
||||
props[name] = { label: field?.label || name, transformer };
|
||||
props[name] = { label: field?.label || name, transformer, interface: field?.interface };
|
||||
}
|
||||
return props;
|
||||
}, {}),
|
||||
@ -78,6 +79,7 @@ export const ChartRenderer: React.FC & {
|
||||
ChartRenderer.Designer = function Designer() {
|
||||
const { t } = useChartsTranslation();
|
||||
const { setVisible, setCurrent } = useContext(ChartConfigContext);
|
||||
const { removeChart } = useContext(ChartDataContext);
|
||||
const { service } = useContext(ChartRendererContext);
|
||||
const field = useField();
|
||||
const schema = useFieldSchema();
|
||||
@ -88,8 +90,8 @@ ChartRenderer.Designer = function Designer() {
|
||||
<SchemaSettingsItem
|
||||
title="Configure"
|
||||
key="configure"
|
||||
onClick={() => {
|
||||
setCurrent({ schema, field, collection: name, service, data: service?.data });
|
||||
onClick={async () => {
|
||||
setCurrent({ schema, field, collection: name, service, data: service.data });
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
@ -109,6 +111,11 @@ ChartRenderer.Designer = function Designer() {
|
||||
breakRemoveOn={{
|
||||
'x-component': 'ChartV2Block',
|
||||
}}
|
||||
confirm={{
|
||||
onOk: () => {
|
||||
removeChart(schema['x-uid']);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { MaybeCollectionProvider, useAPIClient, useRequest } from '@nocobase/client';
|
||||
import React, { createContext } from 'react';
|
||||
import { parseField } from '../utils';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { parseField, removeUnparsableFilter } from '../utils';
|
||||
import { ChartDataContext } from '../block/ChartDataProvider';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import { useChartFilter } from '../hooks';
|
||||
import { ChartFilterContext } from '../filter/FilterProvider';
|
||||
|
||||
export type MeasureProps = {
|
||||
field: string | string[];
|
||||
@ -58,12 +61,23 @@ export const ChartRendererContext = createContext<
|
||||
|
||||
export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
|
||||
const { query, config, collection, transform } = props;
|
||||
const { addChart } = useContext(ChartDataContext);
|
||||
const { ready, form, enabled } = useContext(ChartFilterContext);
|
||||
const { getFilter, hasFilter, appendFilter } = useChartFilter();
|
||||
const schema = useFieldSchema();
|
||||
const api = useAPIClient();
|
||||
const service = useRequest(
|
||||
(collection, query) =>
|
||||
(collection, query, manual) =>
|
||||
new Promise((resolve, reject) => {
|
||||
// Check if the chart is configured
|
||||
if (!(collection && query?.measures?.length)) return resolve(undefined);
|
||||
// If the filter block is enabled, the filter form is required to be rendered
|
||||
if (enabled && !form) return resolve(undefined);
|
||||
const filterValues = getFilter();
|
||||
const queryWithFilter =
|
||||
!manual && hasFilter({ collection, query }, filterValues)
|
||||
? appendFilter({ collection, query }, filterValues)
|
||||
: query;
|
||||
api
|
||||
.request({
|
||||
url: 'charts:query',
|
||||
@ -71,7 +85,8 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
|
||||
data: {
|
||||
uid: schema?.['x-uid'],
|
||||
collection,
|
||||
...query,
|
||||
...queryWithFilter,
|
||||
filter: removeUnparsableFilter(queryWithFilter.filter),
|
||||
dimensions: (query?.dimensions || []).map((item: DimensionProps) => {
|
||||
const dimension = { ...item };
|
||||
if (item.format && !item.alias) {
|
||||
@ -92,27 +107,27 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
|
||||
})
|
||||
.then((res) => {
|
||||
resolve(res?.data?.data);
|
||||
if (!manual && schema?.['x-uid']) {
|
||||
addChart(schema?.['x-uid'], { collection, service, query });
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
}),
|
||||
{
|
||||
defaultParams: [collection, query],
|
||||
// Wait until CharrFilterProvider is rendered and check the status of the filter form
|
||||
// since the filter parameters should be applied if the filter block is enabled
|
||||
ready: ready && (!enabled || !!form),
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<MaybeCollectionProvider collection={collection}>
|
||||
<div
|
||||
className={css`
|
||||
.ant-card {
|
||||
box-shadow: none;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<ConfigProvider card={{ style: { boxShadow: 'none' } }}>
|
||||
<ChartRendererContext.Provider value={{ collection, config, transform, service, query }}>
|
||||
{props.children}
|
||||
</ChartRendererContext.Provider>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
</MaybeCollectionProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Schema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { SelectedField } from './configure/ChartConfigure';
|
||||
import { SelectedField } from './configure';
|
||||
import { FieldOption } from './hooks';
|
||||
import { QueryProps } from './renderer';
|
||||
import lodash from 'lodash';
|
||||
|
||||
export const createRendererSchema = (decoratorProps: any, componentProps = {}) => {
|
||||
const { collection } = decoratorProps;
|
||||
@ -100,3 +101,35 @@ export const processData = (selectedFields: FieldOption[], data: any[], scope: a
|
||||
return processed;
|
||||
});
|
||||
};
|
||||
|
||||
export const removeUnparsableFilter = (filter: any) => {
|
||||
if (typeof filter === 'object' && filter !== null) {
|
||||
if (Array.isArray(filter)) {
|
||||
const newLogic = filter.map((condition) => removeUnparsableFilter(condition)).filter(Boolean);
|
||||
return newLogic.length > 0 ? newLogic : null;
|
||||
} else {
|
||||
const newLogic = {};
|
||||
for (const key in filter) {
|
||||
const value = removeUnparsableFilter(filter[key]);
|
||||
if (value && !(typeof value === 'object' && Object.keys(value).length === 0)) {
|
||||
newLogic[key] = value;
|
||||
}
|
||||
}
|
||||
return Object.keys(newLogic).length > 0 ? newLogic : null;
|
||||
}
|
||||
} else if (typeof filter === 'string' && filter.startsWith('{{$nFilter.') && filter.endsWith('}}')) {
|
||||
return null;
|
||||
}
|
||||
return filter;
|
||||
};
|
||||
|
||||
export const getValuesByPath = (values: any, path: string) => {
|
||||
const keys = path.split('.');
|
||||
let result = lodash.get(values, keys.slice(0, -1).join('.'));
|
||||
if (Array.isArray(result)) {
|
||||
result = result.map((item) => lodash.get(item, keys.slice(-1)[0]));
|
||||
} else {
|
||||
result = lodash.get(result, keys.slice(-1)[0]);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
@ -20,4 +20,15 @@ export default {
|
||||
'Are you sure you want to delete it?': 'Are you sure you want to delete it?',
|
||||
'Sign in button name, which will be displayed on the sign in page':
|
||||
'Sign in button name, which will be displayed on the sign in page',
|
||||
Expand: 'Expand',
|
||||
'Current filter': 'Current filter',
|
||||
'Add custom field': 'Add custom field',
|
||||
'To filter with custom fields, use "Current filter" variables in the chart configuration.':
|
||||
'To filter with custom fields, use "Current filter" variables in the chart configuration.',
|
||||
Input: 'Input',
|
||||
'Date range': 'Date range',
|
||||
'Time range': 'Time range',
|
||||
'Edit field properties': 'Edit field properties',
|
||||
'Select a source field to use metadata of the field': 'Select a source field to use metadata of the field',
|
||||
'Original field': 'Original field',
|
||||
};
|
||||
|
@ -73,4 +73,16 @@ export default {
|
||||
isPercent: '显示为百分比',
|
||||
isGroup: '分组',
|
||||
smooth: '平滑曲线',
|
||||
Collapse: '折叠',
|
||||
Expand: '展开',
|
||||
'Current filter': '当前筛选',
|
||||
'Add custom field': '添加自定义字段',
|
||||
'To filter with custom fields, use "Current filter" variables in the chart configuration.':
|
||||
'要通过自定义字段筛选,需要在图表配置中使用“当前筛选”变量。',
|
||||
Input: '输入框',
|
||||
'Date range': '日期范围',
|
||||
'Time range': '时间范围',
|
||||
'Edit field properties': '编辑字段属性',
|
||||
'Select a source field to use metadata of the field': '选择来源字段可以复用字段的元数据配置',
|
||||
'Original field': '原始字段',
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user