import { RightSquareOutlined } from '@ant-design/icons'; import { ArrayItems, Editable, FormCollapse, FormItem, FormLayout, Switch } from '@formily/antd-v5'; import { Form as FormType, ObjectField, createForm, onFieldChange, onFormInit } from '@formily/core'; import { FormConsumer, ISchema, Schema } from '@formily/react'; import { AutoComplete, FormProvider, SchemaComponent, gridRowColWrap, useCollectionFieldsOptions, useCollectionFilterOptions, useDesignable, } from '@nocobase/client'; import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography } from 'antd'; import { cloneDeep, isEqual } from 'lodash'; import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import { useChartFields, useCollectionOptions, useData, useFieldTypes, useFieldsWithAssociation, useFormatters, useOrderFieldsOptions, useOrderReaction, useTransformers, useTransformersDecimal, } from '../hooks'; import { useChartsTranslation } from '../locale'; import { ChartRenderer, ChartRendererContext } from '../renderer'; 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 { ChartConfigContext } from './ChartConfigProvider'; const { Paragraph, Text } = Typography; export type SelectedField = { field: string | string[]; alias?: string; }; export const ChartConfigure: React.FC<{ insert: ( s: ISchema, options: { onSuccess: () => void; wrap?: (schema: ISchema) => ISchema; }, ) => void; }> & { Renderer: React.FC; Config: React.FC; Query: React.FC; Transform: React.FC; Data: React.FC; } = (props) => { const { t } = useChartsTranslation(); const { service } = useContext(ChartRendererContext); const { visible, setVisible, current } = useContext(ChartConfigContext); const { schema, field, collection, initialValues } = current || {}; const { dn } = useDesignable(); const { modal } = App.useApp(); 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 } = chart.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 chartType = useDefaultChartType(); const form = useMemo( () => createForm({ values: { config: { chartType }, ...(initialValues || field?.decoratorProps), collection }, 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 [field, visible, collection], ); const RunButton: 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 ( { const { query, config, transform, mode } = form.values; const afterSave = () => { setVisible(false); current.service?.run(collection, query); queryRef.current.scrollTop = 0; configRef.current.scrollTop = 0; service.mutate(undefined); }; 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, }); afterSave(); return; } insert(gridRowColWrap(createRendererSchema(rendererProps)), { onSuccess: afterSave, }); }} 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; service.mutate(undefined); }, }); }} width={'95%'} styles={{ body: { 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, data } = current || {}; const { service } = useContext(ChartRendererContext); 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 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 }); const filterOptions = useCollectionFilterOptions(collection); const { service } = useContext(ChartRendererContext); const onCollectionChange = (value: string) => { const { schema, field } = current; setCurrent({ schema, field, collection: value, dataSource: current.dataSource, service: current.service, initialValues: {}, data: undefined, }); service.mutate(undefined); }; const formCollapse = FormCollapse.createFormCollapse(['measures', 'dimensions', 'filter', 'sort']); 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]?.getReference?.(); 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 { service } = useContext(ChartRendererContext); const { current } = useContext(ChartConfigContext); const fields = useFieldsWithAssociation(); const data = useData(current?.data); const error = service?.error; return !error ? ( ({ ...item, _key: index }))} rowKey="_key" 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" /> ) : ( error.message).join('\n') || error.message} showIcon /> ); };