fix(bi): eliminate redundancy queries (#2268)

* fix(bi): eliminate redundancy queries

* fix: reset data after close configure pane

* fix: enum transform bug

* fix: reset form after clicking cancel button

* fix: reset form when changing collection
This commit is contained in:
YANG QIA 2023-07-19 16:34:52 +08:00 committed by GitHub
parent 2d8451f2dd
commit d80213be5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 196 additions and 185 deletions

View File

@ -1,21 +1,23 @@
import { SchemaInitializerButtonContext, useDesignable } from '@nocobase/client'; import { SchemaInitializerButtonContext, useDesignable } from '@nocobase/client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ChartRendererProvider } from '../renderer';
import { ChartConfigContext, ChartConfigCurrent, ChartConfigure } from './ChartConfigure'; import { ChartConfigContext, ChartConfigCurrent, ChartConfigure } from './ChartConfigure';
export const ChartV2Block: React.FC = (props) => { export const ChartV2Block: React.FC = (props) => {
const { insertAdjacent } = useDesignable(); const { insertAdjacent } = useDesignable();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [current, setCurrent] = useState<ChartConfigCurrent>({} as any); const [current, setCurrent] = useState<ChartConfigCurrent>({} as any);
const [data, setData] = useState<string | any[]>([]);
const [initialVisible, setInitialVisible] = useState(false); const [initialVisible, setInitialVisible] = useState(false);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
return ( return (
<SchemaInitializerButtonContext.Provider <SchemaInitializerButtonContext.Provider
value={{ visible: initialVisible, setVisible: setInitialVisible, searchValue, setSearchValue }} value={{ visible: initialVisible, setVisible: setInitialVisible, searchValue, setSearchValue }}
> >
<ChartConfigContext.Provider value={{ visible, setVisible, current, setCurrent, data, setData }}> <ChartConfigContext.Provider value={{ visible, setVisible, current, setCurrent }}>
{props.children} {props.children}
<ChartConfigure insert={(schema, options) => insertAdjacent('beforeEnd', schema, options)} /> <ChartRendererProvider {...current.field?.decoratorProps}>
<ChartConfigure insert={(schema, options) => insertAdjacent('beforeEnd', schema, options)} />
</ChartRendererProvider>
</ChartConfigContext.Provider> </ChartConfigContext.Provider>
</SchemaInitializerButtonContext.Provider> </SchemaInitializerButtonContext.Provider>
); );

View File

@ -8,13 +8,12 @@ import { ChartConfigContext } from './ChartConfigure';
const itemWrap = SchemaInitializer.itemWrap; const itemWrap = SchemaInitializer.itemWrap;
const ConfigureButton = itemWrap((props) => { const ConfigureButton = itemWrap((props) => {
const { setVisible, setCurrent, setData } = useContext(ChartConfigContext); const { setVisible, setCurrent } = useContext(ChartConfigContext);
return ( return (
<SchemaInitializer.Item <SchemaInitializer.Item
{...props} {...props}
onClick={() => { onClick={() => {
setCurrent({ schema: {}, field: null, collection: props.item?.name }); setCurrent({ schema: {}, field: null, collection: props.item?.name, service: null, data: undefined });
setData([]);
setVisible(true); setVisible(true);
}} }}
/> />

View File

@ -20,7 +20,6 @@ import {
import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography } from 'antd'; import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography } from 'antd';
import { cloneDeep, isEqual } from 'lodash'; import { cloneDeep, isEqual } from 'lodash';
import React, { createContext, useContext, useMemo, useRef } from 'react'; import React, { createContext, useContext, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
useChartFields, useChartFields,
useCollectionOptions, useCollectionOptions,
@ -32,8 +31,8 @@ import {
useTransformers, useTransformers,
} from '../hooks'; } from '../hooks';
import { useChartsTranslation } from '../locale'; import { useChartsTranslation } from '../locale';
import { ChartRenderer, ChartRendererProvider, useChartTypes, useCharts } from '../renderer'; import { ChartRenderer, ChartRendererContext, useChartTypes, useCharts, useDefaultChartType } from '../renderer';
import { createRendererSchema, getField, getSelectedFields } from '../utils'; import { createRendererSchema, getField, getSelectedFields, processData } from '../utils';
import { getConfigSchema, querySchema, transformSchema } from './schemas/configure'; import { getConfigSchema, querySchema, transformSchema } from './schemas/configure';
const { Paragraph, Text } = Typography; const { Paragraph, Text } = Typography;
@ -41,6 +40,9 @@ export type ChartConfigCurrent = {
schema: ISchema; schema: ISchema;
field: any; field: any;
collection: string; collection: string;
service: any;
initialValues?: any;
data: any[];
}; };
export type SelectedField = { export type SelectedField = {
@ -53,8 +55,6 @@ export const ChartConfigContext = createContext<{
setVisible?: (visible: boolean) => void; setVisible?: (visible: boolean) => void;
current?: ChartConfigCurrent; current?: ChartConfigCurrent;
setCurrent?: (current: ChartConfigCurrent) => void; setCurrent?: (current: ChartConfigCurrent) => void;
data?: any[] | string;
setData?: (data: any[] | string) => void;
}>({ }>({
visible: true, visible: true,
}); });
@ -68,17 +68,16 @@ export const ChartConfigure: React.FC<{
}, },
) => void; ) => void;
}> & { }> & {
Renderer: React.FC<{ Renderer: React.FC;
runQuery?: any;
}>;
Config: React.FC; Config: React.FC;
Query: React.FC; Query: React.FC;
Transform: React.FC; Transform: React.FC;
Data: React.FC; Data: React.FC;
} = (props) => { } = (props) => {
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
const { service } = useContext(ChartRendererContext);
const { visible, setVisible, current } = useContext(ChartConfigContext); const { visible, setVisible, current } = useContext(ChartConfigContext);
const { schema, field, collection } = current || {}; const { schema, field, collection, initialValues } = current || {};
const { dn } = useDesignable(); const { dn } = useDesignable();
const { modal } = App.useApp(); const { modal } = App.useApp();
const { insert } = props; const { insert } = props;
@ -125,53 +124,48 @@ export const ChartConfigure: React.FC<{
setMeasures(cloneDeep(currentMeasures)); setMeasures(cloneDeep(currentMeasures));
setDimensions(cloneDeep(currentDimensions)); setDimensions(cloneDeep(currentDimensions));
}; };
const chartTypes = useChartTypes(); const chartType = useDefaultChartType();
const form = useMemo( const form = useMemo(
() => { () => {
const chartType = chartTypes[0]?.children?.[0]?.value;
return createForm({ return createForm({
values: { config: { chartType }, ...schema?.['x-decorator-props'], collection, data: '' }, values: { config: { chartType }, ...(initialValues || field?.decoratorProps), collection },
effects: (form) => { effects: (form) => {
onFieldChange('config.chartType', () => initChart(true)); onFieldChange('config.chartType', () => initChart(true));
onFormInit(() => queryReact(form)); onFormInit(() => {
queryReact(form);
});
}, },
}); });
}, },
// visible, collection added here to re-initialize form when visible, collection change // visible, collection added here to re-initialize form when visible, collection change
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[schema, visible, collection], [field, visible, collection],
); );
const runQuery = useRef(null); const RunButton: React.FC = () => (
const RunButton: React.FC = () => { <Button
const [loading, setLoading] = React.useState(false); type="link"
return ( loading={service?.loading}
<Button icon={<RightSquareOutlined />}
type="link" onClick={async () => {
loading={loading} const queryField = form.query('query').take() as ObjectField;
icon={<RightSquareOutlined />} try {
onClick={async () => { await queryField?.validate();
const queryField = form.query('query').take() as ObjectField; } catch (e) {
try { return;
await queryField?.validate(); }
} catch (e) {
return;
}
setLoading(true); try {
try { await service.runAsync(collection, form.values.query);
await runQuery?.current(form.values.query); } catch (e) {
} catch (e) { console.log(e);
console.log(e); }
} queryReact(form, initChart);
queryReact(form, initChart); }}
setLoading(false); >
}} {t('Run query')}
> </Button>
{t('Run query')} );
</Button>
);
};
const queryRef = useRef(null); const queryRef = useRef(null);
const configRef = useRef(null); const configRef = useRef(null);
@ -181,6 +175,13 @@ export const ChartConfigure: React.FC<{
open={visible} open={visible}
onOk={() => { onOk={() => {
const { query, config, transform, mode } = form.values; 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 = { const rendererProps = {
query, query,
config, config,
@ -195,13 +196,11 @@ export const ChartConfigure: React.FC<{
dn.emit('patch', { dn.emit('patch', {
schema, schema,
}); });
setVisible(false); afterSave();
queryRef.current.scrollTop = 0;
configRef.current.scrollTop = 0;
return; return;
} }
insert(createRendererSchema(rendererProps), { insert(createRendererSchema(rendererProps), {
onSuccess: () => setVisible(false), onSuccess: afterSave,
wrap: gridRowColWrap, wrap: gridRowColWrap,
}); });
}} }}
@ -216,6 +215,7 @@ export const ChartConfigure: React.FC<{
setVisible(false); setVisible(false);
queryRef.current.scrollTop = 0; queryRef.current.scrollTop = 0;
configRef.current.scrollTop = 0; configRef.current.scrollTop = 0;
service.mutate(undefined);
}, },
}); });
}} }}
@ -231,6 +231,7 @@ export const ChartConfigure: React.FC<{
style={{ style={{
height: 'calc(100vh - 300px)', height: 'calc(100vh - 300px)',
overflow: 'auto', overflow: 'auto',
margin: '12px 0 12px 12px',
}} }}
ref={queryRef} ref={queryRef}
> >
@ -256,6 +257,7 @@ export const ChartConfigure: React.FC<{
style={{ style={{
height: 'calc(100vh - 300px)', height: 'calc(100vh - 300px)',
overflow: 'auto', overflow: 'auto',
margin: '12px 3px 12px 3px',
}} }}
ref={configRef} ref={configRef}
> >
@ -276,8 +278,12 @@ export const ChartConfigure: React.FC<{
</Card> </Card>
</Col> </Col>
<Col span={11}> <Col span={11}>
<Card> <Card
<ChartConfigure.Renderer runQuery={runQuery} /> style={{
margin: '12px 12px 12px 0',
}}
>
<ChartConfigure.Renderer />
</Card> </Card>
</Col> </Col>
</Row> </Row>
@ -288,7 +294,8 @@ export const ChartConfigure: React.FC<{
ChartConfigure.Renderer = function Renderer(props) { ChartConfigure.Renderer = function Renderer(props) {
const { current } = useContext(ChartConfigContext); const { current } = useContext(ChartConfigContext);
const { collection } = current || {}; const { collection, data } = current || {};
const { service } = useContext(ChartRendererContext);
return ( return (
<FormConsumer> <FormConsumer>
{(form) => { {(form) => {
@ -297,15 +304,9 @@ ChartConfigure.Renderer = function Renderer(props) {
const config = cloneDeep(form.values.config); const config = cloneDeep(form.values.config);
const transform = cloneDeep(form.values.transform); const transform = cloneDeep(form.values.transform);
return ( return (
<ChartRendererProvider <ChartRendererContext.Provider value={{ collection, config, transform, service, data }}>
collection={collection} <ChartRenderer {...props} />
query={form.values.query} </ChartRendererContext.Provider>
config={config}
transform={transform}
mode={form.values.mode}
>
<ChartRenderer configuring={true} {...props} />
</ChartRendererProvider>
); );
}} }}
</FormConsumer> </FormConsumer>
@ -314,27 +315,30 @@ ChartConfigure.Renderer = function Renderer(props) {
ChartConfigure.Query = function Query() { ChartConfigure.Query = function Query() {
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
const { t: lang } = useTranslation();
const fields = useFieldsWithAssociation(); const fields = useFieldsWithAssociation();
const useFormatterOptions = useFormatters(fields); const useFormatterOptions = useFormatters(fields);
const collectionOptions = useCollectionOptions(); const collectionOptions = useCollectionOptions();
const { current, setCurrent } = useContext(ChartConfigContext); const { current, setCurrent } = useContext(ChartConfigContext);
const { collection } = current || {}; const { collection } = current || {};
const fieldOptions = useCollectionFieldsOptions(collection, 1); const fieldOptions = useCollectionFieldsOptions(collection, 1);
const compiledFieldOptions = Schema.compile(fieldOptions, { t: lang }); const compiledFieldOptions = Schema.compile(fieldOptions, { t });
const filterOptions = useCollectionFilterOptions(collection); const filterOptions = useCollectionFilterOptions(collection);
const formCollapse = FormCollapse.createFormCollapse(['measures', 'dimensions', 'filter', 'sort']);
const { service } = useContext(ChartRendererContext);
const onCollectionChange = (value: string) => { const onCollectionChange = (value: string) => {
const { schema, field } = current; const { schema, field } = current;
const newSchema = { ...schema };
newSchema['x-decorator-props'] = { collection: value };
newSchema['x-acl-action'] = `${value}:list`;
setCurrent({ setCurrent({
schema: newSchema, schema,
field, field,
collection: value, collection: value,
service: current.service,
initialValues: {},
data: undefined,
}); });
service.mutate(undefined);
}; };
const formCollapse = FormCollapse.createFormCollapse(['measures', 'dimensions', 'filter', 'sort']);
const FromSql = () => ( const FromSql = () => (
<Text code> <Text code>
From <span style={{ color: '#1890ff' }}>{current?.collection}</span> From <span style={{ color: '#1890ff' }}>{current?.collection}</span>
@ -430,22 +434,38 @@ ChartConfigure.Transform = function Transform() {
}; };
ChartConfigure.Data = function Data() { ChartConfigure.Data = function Data() {
const { data } = useContext(ChartConfigContext); const { t } = useChartsTranslation();
const { current } = useContext(ChartConfigContext);
const { service } = useContext(ChartRendererContext);
const fields = useFieldsWithAssociation(); const fields = useFieldsWithAssociation();
return Array.isArray(data) ? ( const data = processData(fields, service?.data || current?.data || [], { t });
<Table const error = service?.error;
dataSource={data} return !error ? (
columns={Object.keys(data[0] || {}).map((col) => { <div
const field = getField(fields, col.split('.')); style={{
return { overflowX: 'auto',
title: field?.label || col, overflowY: 'hidden',
dataIndex: col, }}
key: col, >
}; <Table
})} dataSource={data}
size="small" 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>
) : ( ) : (
<Alert message="Error" type="error" description={data} showIcon /> <Alert
message="Error"
type="error"
description={error?.response?.data?.errors?.map?.((error: any) => error.message).join('\n') || error.message}
showIcon
/>
); );
}; };

View File

@ -90,6 +90,11 @@ export const useChartTypes = (): {
}, []); }, []);
}; };
export const useDefaultChartType = () => {
const chartTypes = useChartTypes();
return chartTypes[0]?.children?.[0]?.value;
};
export const useToggleChartLibrary = () => { export const useToggleChartLibrary = () => {
const ctx = useContext(ChartLibraryContext); const ctx = useContext(ChartLibraryContext);
return { return {

View File

@ -1,107 +1,34 @@
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { import {
GeneralSchemaDesigner, GeneralSchemaDesigner,
gridRowColWrap,
SchemaSettings, SchemaSettings,
gridRowColWrap,
useAPIClient, useAPIClient,
useCollection, useCollection,
useDesignable, useDesignable,
useRequest,
} from '@nocobase/client'; } from '@nocobase/client';
import { Empty, Result, Typography } from 'antd'; import { Empty, Result, Spin, Typography } from 'antd';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { ChartConfigContext } from '../block'; import { ChartConfigContext } from '../block';
import { useFieldsWithAssociation, useFieldTransformer } from '../hooks'; import { useFieldTransformer, useFieldsWithAssociation } from '../hooks';
import { useChartsTranslation } from '../locale'; import { useChartsTranslation } from '../locale';
import { createRendererSchema, getField, parseField, processData } from '../utils'; import { createRendererSchema, getField, processData } from '../utils';
import { useCharts } from './ChartLibrary'; import { useCharts } from './ChartLibrary';
import { ChartRendererContext, DimensionProps, MeasureProps, QueryProps } from './ChartRendererProvider'; import { ChartRendererContext } from './ChartRendererProvider';
const { Paragraph, Text } = Typography; const { Paragraph, Text } = Typography;
export const ChartRenderer: React.FC<{ export const ChartRenderer: React.FC & {
configuring?: boolean;
runQuery?: any;
}> & {
Designer: React.FC; Designer: React.FC;
} = (props) => { } = (props) => {
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
const { setData: setQueryData, current } = useContext(ChartConfigContext); const ctx = useContext(ChartRendererContext);
const { query, config, collection, transform } = useContext(ChartRendererContext); const { config, transform, collection, service, data: _data } = ctx;
const { configuring, runQuery } = props; const fields = useFieldsWithAssociation(collection);
const data = processData(fields, service?.data || _data || [], { t });
const general = config?.general || {}; const general = config?.general || {};
const advanced = config?.advanced || {}; const advanced = config?.advanced || {};
const schema = useFieldSchema();
const currentSchema = schema || current?.schema;
const fields = useFieldsWithAssociation(collection);
const api = useAPIClient(); const api = useAPIClient();
const [data, setData] = useState<any[]>([]);
const { runAsync } = useRequest(
(query) =>
api
.request({
url: 'charts:query',
method: 'POST',
data: {
uid: currentSchema?.['x-uid'],
collection,
...query,
dimensions: (query?.dimensions || []).map((item: DimensionProps) => {
const dimension = { ...item };
if (item.format && !item.alias) {
const { alias } = parseField(item.field);
dimension.alias = alias;
}
return dimension;
}),
measures: (query?.measures || []).map((item: MeasureProps) => {
const measure = { ...item };
if (item.aggregation && !item.alias) {
const { alias } = parseField(item.field);
measure.alias = alias;
}
return measure;
}),
},
})
.then((res) => {
const data = res?.data?.data || [];
return processData(fields, data, { t });
}),
{
manual: true,
onSuccess: (data) => {
setData(data);
},
onFinally(params, data, error: any) {
if (!configuring) {
return;
}
if (error) {
const message = error?.response?.data?.errors?.map?.((error: any) => error.message).join('\n');
setQueryData(message || error.message);
return;
}
setQueryData(data);
},
},
);
useEffect(() => {
setData([]);
const run = async (query: QueryProps) => {
if (
query?.measures?.length
// || (query?.sql?.fields && query?.sql?.clauses)
) {
await runAsync(query);
}
};
if (runQuery) {
runQuery.current = run;
}
run(query);
}, [query, runAsync, runQuery]);
const charts = useCharts(); const charts = useCharts();
const chart = charts[config?.chartType]; const chart = charts[config?.chartType];
@ -137,16 +64,21 @@ export const ChartRenderer: React.FC<{
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('Please configure chart')} /> <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('Please configure chart')} />
); );
return data && data.length ? ( if (service.loading) {
<C /> return <Spin />;
) : ( }
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('Please configure and run query')} />
); if (!(data && data.length)) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('Please configure and run query')} />;
}
return <C />;
}; };
ChartRenderer.Designer = function Designer() { ChartRenderer.Designer = function Designer() {
const { t } = useChartsTranslation(); const { t } = useChartsTranslation();
const { setVisible, setCurrent } = useContext(ChartConfigContext); const { setVisible, setCurrent } = useContext(ChartConfigContext);
const { service } = useContext(ChartRendererContext);
const field = useField(); const field = useField();
const schema = useFieldSchema(); const schema = useFieldSchema();
const { insertAdjacent } = useDesignable(); const { insertAdjacent } = useDesignable();
@ -156,7 +88,7 @@ ChartRenderer.Designer = function Designer() {
<SchemaSettings.Item <SchemaSettings.Item
key="configure" key="configure"
onClick={() => { onClick={() => {
setCurrent({ schema, field, collection: name }); setCurrent({ schema, field, collection: name, service, data: service?.data });
setVisible(true); setVisible(true);
}} }}
> >

View File

@ -1,6 +1,8 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { MaybeCollectionProvider } from '@nocobase/client'; import { useFieldSchema } from '@formily/react';
import { MaybeCollectionProvider, useAPIClient, useRequest } from '@nocobase/client';
import React, { createContext } from 'react'; import React, { createContext } from 'react';
import { parseField } from '../utils';
export type MeasureProps = { export type MeasureProps = {
field: string | string[]; field: string | string[];
@ -47,10 +49,62 @@ export type ChartRendererProps = {
mode?: 'builder' | 'sql'; mode?: 'builder' | 'sql';
}; };
export const ChartRendererContext = createContext<ChartRendererProps>({} as any); export const ChartRendererContext = createContext<{
collection: string;
config?: {
chartType: string;
general: any;
advanced: any;
};
transform?: TransformProps[];
service: any;
data?: any[];
}>({} as any);
export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => { export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
const { collection } = props; const { query, config, collection, transform } = props;
const schema = useFieldSchema();
const api = useAPIClient();
const service = useRequest(
(collection, query) =>
new Promise((resolve, reject) => {
if (!(collection && query?.measures?.length)) return resolve(undefined);
api
.request({
url: 'charts:query',
method: 'POST',
data: {
uid: schema?.['x-uid'],
collection,
...query,
dimensions: (query?.dimensions || []).map((item: DimensionProps) => {
const dimension = { ...item };
if (item.format && !item.alias) {
const { alias } = parseField(item.field);
dimension.alias = alias;
}
return dimension;
}),
measures: (query?.measures || []).map((item: MeasureProps) => {
const measure = { ...item };
if (item.aggregation && !item.alias) {
const { alias } = parseField(item.field);
measure.alias = alias;
}
return measure;
}),
},
})
.then((res) => {
resolve(res?.data?.data);
})
.catch(reject);
}),
{
defaultParams: [collection, query],
},
);
return ( return (
<MaybeCollectionProvider collection={collection}> <MaybeCollectionProvider collection={collection}>
<div <div
@ -58,12 +112,11 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
.ant-card { .ant-card {
box-shadow: none; box-shadow: none;
} }
.ant-card-body {
padding: 0;
}
`} `}
> >
<ChartRendererContext.Provider value={{ ...props }}>{props.children}</ChartRendererContext.Provider> <ChartRendererContext.Provider value={{ collection, config, transform, service }}>
{props.children}
</ChartRendererContext.Provider>
</div> </div>
</MaybeCollectionProvider> </MaybeCollectionProvider>
); );

View File

@ -181,7 +181,7 @@ export const G2PlotLibrary: Charts = {
'x-reactions': '{{ useChartFields }}', 'x-reactions': '{{ useChartFields }}',
'x-component-props': { 'x-component-props': {
style: { style: {
'min-width': '200px', minWidth: '200px',
}, },
}, },
required: true, required: true,