import { RightSquareOutlined } from '@ant-design/icons'; import { ArrayItems, Editable, Form, FormCollapse, FormItem, Switch } from '@formily/antd'; import { createForm, Form as FormType, ObjectField, onFieldChange, onFormInit } from '@formily/core'; import { FormConsumer, ISchema, Schema } from '@formily/react'; import { AutoComplete, Cascader, DatePicker, Filter, gridRowColWrap, Input, InputNumber, Radio, SchemaComponent, Select, useCollectionFieldsOptions, useCollectionFilterOptions, useDesignable, } from '@nocobase/client'; import { Alert, 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 { useTranslation } from 'react-i18next'; import { useChartFields, useCollectionOptions, useFieldsWithAssociation, useFieldTypes, useFormatters, useOrderFieldsOptions, useOrderReaction, useTransformers, } from '../hooks'; import { useChartsTranslation } from '../locale'; import { ChartRenderer, ChartRendererProvider, useCharts, useChartTypes } from '../renderer'; import { createRendererSchema, getField, getSelectedFields } from '../utils'; import { getConfigSchema, querySchema, transformSchema } from './schemas/configure'; const { Paragraph, Text } = Typography; export type ChartConfigCurrent = { schema: ISchema; field: any; collection: string; }; export type SelectedField = { field: string | string[]; alias?: string; }; export const ChartConfigContext = createContext<{ visible: boolean; setVisible?: (visible: boolean) => void; current?: ChartConfigCurrent; setCurrent?: (current: ChartConfigCurrent) => void; data?: any[] | string; setData?: (data: any[] | string) => void; }>({ visible: true, }); export const ChartConfigure: React.FC<{ insert: ( s: ISchema, options: { onSuccess: () => void; wrap?: (schema: ISchema) => ISchema; }, ) => void; }> & { Renderer: React.FC<{ runQuery?: any; }>; Config: React.FC; Query: React.FC; Transform: React.FC; Data: React.FC; } = (props) => { const { t } = useChartsTranslation(); const { visible, setVisible, current } = useContext(ChartConfigContext); const { schema, field, collection } = current || {}; const { dn } = useDesignable(); const { insert } = props; const charts = useCharts(); const fields = useFieldsWithAssociation(collection); const initChart = (overwrite = false) => { if (!form.modified) { return; } const chartType = form.values.config?.chartType; if (!chartType) { return; } const chart = charts[chartType]; const init = chart?.init; if (!init) { if (overwrite) { form.values.config.general = {}; form.values.config.advanced = {}; } return; } const query = form.values.query; const selectedFields = getSelectedFields(fields, query); const { general, advanced } = init(selectedFields, query); if (general || overwrite) { form.values.config.general = general; } if (advanced || overwrite) { form.values.config.advanced = advanced || {}; } }; const [measures, setMeasures] = React.useState([]); const [dimensions, setDimensions] = React.useState([]); const queryReact = (form: FormType, reaction?: () => void) => { const currentMeasures = form.values.query?.measures.filter((item) => item.field) || []; const currentDimensions = form.values.query?.dimensions.filter((item) => item.field) || []; if (isEqual(currentMeasures, measures) && isEqual(currentDimensions, dimensions)) { return; } reaction?.(); setMeasures(cloneDeep(currentMeasures)); setDimensions(cloneDeep(currentDimensions)); }; const chartTypes = useChartTypes(); const form = useMemo( () => { const chartType = chartTypes[0]?.children?.[0]?.value; return createForm({ values: { config: { chartType }, ...schema?.['x-decorator-props'], collection, data: '' }, effects: (form) => { onFieldChange('config.chartType', () => initChart(true)); onFormInit(() => queryReact(form)); }, }); }, // visible, collection added here to re-initialize form when visible, collection change // eslint-disable-next-line react-hooks/exhaustive-deps [schema, visible, collection], ); const runQuery = useRef(null); const RunButton: React.FC = () => { const [loading, setLoading] = React.useState(false); return ( ); }; const queryRef = useRef(null); const configRef = useRef(null); return ( { const { query, config, transform, mode } = form.values; const rendererProps = { query, config, collection, transform, mode: mode || 'builder', }; if (schema && schema['x-uid']) { schema['x-decorator-props'] = rendererProps; field.decoratorProps = rendererProps; field['x-acl-action'] = `${collection}:list`; dn.emit('patch', { schema, }); setVisible(false); queryRef.current.scrollTop = 0; configRef.current.scrollTop = 0; return; } insert(createRendererSchema(rendererProps), { onSuccess: () => setVisible(false), wrap: gridRowColWrap, }); }} onCancel={() => { Modal.confirm({ title: t('Are you sure to cancel?'), content: t('You changes are not saved. If you click OK, your changes will be lost.'), okButtonProps: { danger: true, }, onOk: () => { setVisible(false); queryRef.current.scrollTop = 0; configRef.current.scrollTop = 0; }, }); }} width={'95%'} bodyStyle={{ background: 'rgba(128, 128, 128, 0.08)', }} >
} items={[ { label: t('Query'), key: 'query', children: , }, { label: t('Data'), key: 'data', children: , }, ]} /> , }, { label: t('Transform'), key: 'transform', children: , }, ]} />
); }; ChartConfigure.Renderer = function Renderer(props) { const { current } = useContext(ChartConfigContext); const { collection } = current || {}; return ( {(form) => { // Any change of config and transform will trigger rerender // Change of query only trigger rerender when "Run query" button is clicked const config = cloneDeep(form.values.config); const transform = cloneDeep(form.values.transform); return ( ); }} ); }; ChartConfigure.Query = function Query() { const { t } = useChartsTranslation(); const { t: lang } = useTranslation(); const fields = useFieldsWithAssociation(); const useFormatterOptions = useFormatters(fields); const collectionOptions = useCollectionOptions(); const { current, setCurrent } = useContext(ChartConfigContext); const { collection } = current || {}; const fieldOptions = useCollectionFieldsOptions(collection, 1); const compiledFieldOptions = Schema.compile(fieldOptions, { t: lang }); const filterOptions = useCollectionFilterOptions(collection); const formCollapse = FormCollapse.createFormCollapse(['measures', 'dimensions', 'filter', 'sort']); const onCollectionChange = (value: string) => { const { schema, field } = current; const newSchema = { ...schema }; newSchema['x-decorator-props'] = { collection: value }; newSchema['x-acl-action'] = `${value}:list`; setCurrent({ schema: newSchema, field, collection: value, }); }; const FromSql = () => ( From {current?.collection} ); return ( ); }; ChartConfigure.Config = function Config() { const { t } = useChartsTranslation(); const chartTypes = useChartTypes(); const fields = useFieldsWithAssociation(); const charts = useCharts(); const getChartFields = useChartFields(fields); const getReference = (chartType: string) => { const reference = charts[chartType]?.reference; if (!reference) return ''; const { title, link } = reference; return ( {t('Config reference: ')} {t(title)} ); }; return ( {(form) => { const chartType = form.values.config?.chartType; const chart = charts[chartType]; const schema = chart?.schema || {}; return ( ); }} ); }; ChartConfigure.Transform = function Transform() { const { t } = useChartsTranslation(); const fields = useFieldsWithAssociation(); const useFieldTypeOptions = useFieldTypes(fields); const getChartFields = useChartFields(fields); return ( ); }; ChartConfigure.Data = function Data() { const { data } = useContext(ChartConfigContext); const fields = useFieldsWithAssociation(); return Array.isArray(data) ? ( { const field = getField(fields, col.split('.')); return { title: field?.label || col, dataIndex: col, key: col, }; })} size="small" /> ) : ( ); };