feat(bi): allow to use variables in query filter (#2609)
* chore: rearrange hooks * chore: allow parse filter from params.values * feat: support use variables when using chart filter * refactor: improve query code by composing process middlwares * chore: `$date` -> `$nDate`
This commit is contained in:
		
							parent
							
								
									b655517a74
								
							
						
					
					
						commit
						f82b466aaa
					
				| @ -120,9 +120,8 @@ const ConstantTypes = { | |||||||
| 
 | 
 | ||||||
| function getTypedConstantOption(type: string, types: true | string[], fieldNames) { | function getTypedConstantOption(type: string, types: true | string[], fieldNames) { | ||||||
|   const allTypes = Object.values(ConstantTypes); |   const allTypes = Object.values(ConstantTypes); | ||||||
|   const children = (types |   const children = ( | ||||||
|     ? allTypes.filter((item) => (Array.isArray(types) && types.includes(item.value)) || types === true) |     types ? allTypes.filter((item) => (Array.isArray(types) && types.includes(item.value)) || types === true) : allTypes | ||||||
|     : allTypes |  | ||||||
|   ).map((item) => |   ).map((item) => | ||||||
|     Object.keys(item).reduce( |     Object.keys(item).reduce( | ||||||
|       (result, key) => |       (result, key) => | ||||||
| @ -182,29 +181,27 @@ export function Input(props) { | |||||||
|     fieldNames ?? {}, |     fieldNames ?? {}, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const { |   const { component: ConstantComponent, ...constantOption }: DefaultOptionType & { component?: React.FC<any> } = | ||||||
|     component: ConstantComponent, |     useMemo(() => { | ||||||
|     ...constantOption |       if (children) { | ||||||
|   }: DefaultOptionType & { component?: React.FC<any> } = useMemo(() => { |         return { | ||||||
|     if (children) { |           value: '', | ||||||
|  |           label: t('Constant'), | ||||||
|  |           [names.value]: '', | ||||||
|  |           [names.label]: t('Constant'), | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |       if (useTypedConstant) { | ||||||
|  |         return getTypedConstantOption(type, useTypedConstant, names); | ||||||
|  |       } | ||||||
|       return { |       return { | ||||||
|         value: '', |         value: '', | ||||||
|         label: t('Constant'), |         label: t('Null'), | ||||||
|         [names.value]: '', |         [names.value]: '', | ||||||
|         [names.label]: t('Constant'), |         [names.label]: t('Null'), | ||||||
|  |         component: ConstantTypes.null.component, | ||||||
|       }; |       }; | ||||||
|     } |     }, [type, useTypedConstant]); | ||||||
|     if (useTypedConstant) { |  | ||||||
|       return getTypedConstantOption(type, useTypedConstant, names); |  | ||||||
|     } |  | ||||||
|     return { |  | ||||||
|       value: '', |  | ||||||
|       label: t('Null'), |  | ||||||
|       [names.value]: '', |  | ||||||
|       [names.label]: t('Null'), |  | ||||||
|       component: ConstantTypes.null.component, |  | ||||||
|     }; |  | ||||||
|   }, [type, useTypedConstant]); |  | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     setOptions([compile(constantOption), ...(scope ? cloneDeep(scope) : [])]); |     setOptions([compile(constantOption), ...(scope ? cloneDeep(scope) : [])]); | ||||||
| @ -298,6 +295,7 @@ export function Input(props) { | |||||||
|                   margin: 0; |                   margin: 0; | ||||||
|                   padding: 2px 7px; |                   padding: 2px 7px; | ||||||
|                   border-radius: 10px; |                   border-radius: 10px; | ||||||
|  |                   white-space: nowrap; | ||||||
|                 } |                 } | ||||||
|               } |               } | ||||||
|             `,
 |             `,
 | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import { getDateVars, parseFilter } from '@nocobase/utils'; | import { getDateVars, parseFilter } from '@nocobase/utils'; | ||||||
|  | import lodash from 'lodash'; | ||||||
| 
 | 
 | ||||||
| function getUser(ctx) { | function getUser(ctx) { | ||||||
|   return async ({ fields }) => { |   return async ({ fields }) => { | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ | |||||||
|     "@testing-library/react": "^14.0.0", |     "@testing-library/react": "^14.0.0", | ||||||
|     "antd": "5.x", |     "antd": "5.x", | ||||||
|     "classnames": "^2.3.1", |     "classnames": "^2.3.1", | ||||||
|  |     "koa-compose": "^4.1.0", | ||||||
|     "lodash": "^4.17.21", |     "lodash": "^4.17.21", | ||||||
|     "react": "^18.2.0", |     "react": "^18.2.0", | ||||||
|     "react-error-boundary": "^4.0.10", |     "react-error-boundary": "^4.0.10", | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| 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 { ChartRendererProvider } from '../renderer'; | ||||||
| import { ChartConfigContext, ChartConfigCurrent, ChartConfigure } from './ChartConfigure'; | import { ChartConfigContext, ChartConfigCurrent, ChartConfigure } from '../configure/ChartConfigure'; | ||||||
| 
 | 
 | ||||||
| export const ChartV2Block: React.FC = (props) => { | export const ChartV2Block: React.FC = (props) => { | ||||||
|   const { insertAdjacent } = useDesignable(); |   const { insertAdjacent } = useDesignable(); | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ import { uid } from '@formily/shared'; | |||||||
| import { SchemaInitializer, useACLRoleContext, useCollectionDataSourceItems } from '@nocobase/client'; | import { SchemaInitializer, useACLRoleContext, useCollectionDataSourceItems } from '@nocobase/client'; | ||||||
| import React, { useContext } from 'react'; | import React, { useContext } from 'react'; | ||||||
| import { useChartsTranslation } from '../locale'; | import { useChartsTranslation } from '../locale'; | ||||||
| import { ChartConfigContext } from './ChartConfigure'; | import { ChartConfigContext } from '../configure/ChartConfigure'; | ||||||
| 
 | 
 | ||||||
| const itemWrap = SchemaInitializer.itemWrap; | const itemWrap = SchemaInitializer.itemWrap; | ||||||
| const ConfigureButton = itemWrap((props) => { | const ConfigureButton = itemWrap((props) => { | ||||||
|  | |||||||
| @ -1,4 +1,3 @@ | |||||||
| export * from './ChartBlock'; | export * from './ChartBlock'; | ||||||
| export * from './ChartBlockDesigner'; | export * from './ChartBlockDesigner'; | ||||||
| export * from './ChartBlockInitializer'; | export * from './ChartBlockInitializer'; | ||||||
| export * from './ChartConfigure'; |  | ||||||
|  | |||||||
| @ -30,6 +30,7 @@ import { ChartRenderer, ChartRendererContext } from '../renderer'; | |||||||
| import { createRendererSchema, getField, getSelectedFields } from '../utils'; | import { createRendererSchema, getField, getSelectedFields } from '../utils'; | ||||||
| import { getConfigSchema, querySchema, transformSchema } from './schemas/configure'; | import { getConfigSchema, querySchema, transformSchema } from './schemas/configure'; | ||||||
| import { useChartTypes, useCharts, useDefaultChartType } from '../chart/library'; | import { useChartTypes, useCharts, useDefaultChartType } from '../chart/library'; | ||||||
|  | import { FilterDynamicComponent } from './FilterDynamicComponent'; | ||||||
| const { Paragraph, Text } = Typography; | const { Paragraph, Text } = Typography; | ||||||
| 
 | 
 | ||||||
| export type ChartConfigCurrent = { | export type ChartConfigCurrent = { | ||||||
| @ -353,7 +354,7 @@ ChartConfigure.Query = function Query() { | |||||||
|         collection: current?.collection, |         collection: current?.collection, | ||||||
|         useOrderReaction: useOrderReaction(compiledFieldOptions, fields), |         useOrderReaction: useOrderReaction(compiledFieldOptions, fields), | ||||||
|       }} |       }} | ||||||
|       components={{ ArrayItems, Editable, FormCollapse, FormItem, Space, Switch, FromSql }} |       components={{ ArrayItems, Editable, FormCollapse, FormItem, Space, Switch, FromSql, FilterDynamicComponent }} | ||||||
|     /> |     /> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| @ -0,0 +1,14 @@ | |||||||
|  | import { Variable } from '@nocobase/client'; | ||||||
|  | import React from 'react'; | ||||||
|  | import { useVariableOptions } from '../hooks'; | ||||||
|  | 
 | ||||||
|  | export function FilterDynamicComponent(props) { | ||||||
|  |   const { value, onChange, renderSchemaComponent } = props; | ||||||
|  |   const options = useVariableOptions(); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <Variable.Input value={value} onChange={onChange} scope={options}> | ||||||
|  |       {renderSchemaComponent()} | ||||||
|  |     </Variable.Input> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @ -315,7 +315,7 @@ export const querySchema: ISchema = { | |||||||
|                       'x-component': 'Filter', |                       'x-component': 'Filter', | ||||||
|                       'x-component-props': { |                       'x-component-props': { | ||||||
|                         options: '{{ filterOptions }}', |                         options: '{{ filterOptions }}', | ||||||
|                         // dynamicComponent: 'Input',
 |                         dynamicComponent: 'FilterDynamicComponent', | ||||||
|                       }, |                       }, | ||||||
|                     }, |                     }, | ||||||
|                   }, |                   }, | ||||||
| @ -0,0 +1,3 @@ | |||||||
|  | export * from './query'; | ||||||
|  | export * from './transformer'; | ||||||
|  | export * from './useVariableOptions'; | ||||||
| @ -3,12 +3,11 @@ import { ISchema, Schema, useForm } from '@formily/react'; | |||||||
| import { CollectionFieldOptions, useACLRoleContext, useCollectionManager } from '@nocobase/client'; | import { CollectionFieldOptions, useACLRoleContext, useCollectionManager } from '@nocobase/client'; | ||||||
| import { useContext } from 'react'; | import { useContext } from 'react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { ChartConfigContext } from './block/ChartConfigure'; | import { ChartConfigContext } from '../configure/ChartConfigure'; | ||||||
| import formatters from './block/formatters'; | import formatters from '../block/formatters'; | ||||||
| import transformers from './block/transformers'; | import { useChartsTranslation } from '../locale'; | ||||||
| import { lang, useChartsTranslation } from './locale'; | import { ChartRendererContext } from '../renderer'; | ||||||
| import { ChartRendererContext, ChartRendererProps } from './renderer'; | import { getField, getSelectedFields, parseField, processData } from '../utils'; | ||||||
| import { getField, getSelectedFields, parseField, processData } from './utils'; |  | ||||||
| 
 | 
 | ||||||
| export type FieldOption = { | export type FieldOption = { | ||||||
|   value: string; |   value: string; | ||||||
| @ -143,71 +142,6 @@ export const useCollectionOptions = () => { | |||||||
|   return Schema.compile(options, { t }); |   return Schema.compile(options, { t }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** |  | ||||||
|  * useFieldTypes |  | ||||||
|  * Get field types for using transformers |  | ||||||
|  * Only supported types will be displayed |  | ||||||
|  * Some interfaces and types will be mapped to supported types |  | ||||||
|  */ |  | ||||||
| export const useFieldTypes = (fields: FieldOption[]) => (field: any) => { |  | ||||||
|   const selectedField = field.query('.field').get('value'); |  | ||||||
|   const query = field.query('query').get('value') || {}; |  | ||||||
|   const selectedFields = getSelectedFields(fields, query); |  | ||||||
|   const fieldProps = selectedFields.find((field) => field.value === selectedField); |  | ||||||
|   const supports = Object.keys(transformers); |  | ||||||
|   field.dataSource = supports.map((key) => ({ |  | ||||||
|     label: lang(key), |  | ||||||
|     value: key, |  | ||||||
|   })); |  | ||||||
|   const map = { |  | ||||||
|     createdAt: 'datetime', |  | ||||||
|     updatedAt: 'datetime', |  | ||||||
|     double: 'number', |  | ||||||
|     integer: 'number', |  | ||||||
|     percent: 'number', |  | ||||||
|   }; |  | ||||||
|   const fieldInterface = fieldProps?.interface; |  | ||||||
|   const fieldType = fieldProps?.type; |  | ||||||
|   const key = map[fieldInterface] || map[fieldType] || fieldType; |  | ||||||
|   if (supports.includes(key)) { |  | ||||||
|     field.setState({ |  | ||||||
|       value: key, |  | ||||||
|       disabled: true, |  | ||||||
|     }); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   field.setState({ |  | ||||||
|     value: null, |  | ||||||
|     disabled: false, |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const useTransformers = (field: any) => { |  | ||||||
|   const selectedType = field.query('.type').get('value'); |  | ||||||
|   if (!selectedType) { |  | ||||||
|     field.dataSource = []; |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   const options = Object.keys(transformers[selectedType] || {}).map((key) => ({ |  | ||||||
|     label: lang(key), |  | ||||||
|     value: key, |  | ||||||
|   })); |  | ||||||
|   field.dataSource = options; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const useFieldTransformer = (transform: ChartRendererProps['transform'], locale = 'en-US') => { |  | ||||||
|   return (transform || []) |  | ||||||
|     .filter((item) => item.field && item.type && item.format) |  | ||||||
|     .reduce((mp, item) => { |  | ||||||
|       const transformer = transformers[item.type][item.format]; |  | ||||||
|       if (!transformer) { |  | ||||||
|         return mp; |  | ||||||
|       } |  | ||||||
|       mp[item.field] = (val: any) => transformer(val, locale); |  | ||||||
|       return mp; |  | ||||||
|     }, {}); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const useOrderFieldsOptions = (defaultOptions: any[], fields: FieldOption[]) => (field: any) => { | export const useOrderFieldsOptions = (defaultOptions: any[], fields: FieldOption[]) => (field: any) => { | ||||||
|   const query = field.query('query').get('value') || {}; |   const query = field.query('query').get('value') || {}; | ||||||
|   const { measures = [] } = query; |   const { measures = [] } = query; | ||||||
| @ -0,0 +1,70 @@ | |||||||
|  | import transformers from '../block/transformers'; | ||||||
|  | import { lang } from '../locale'; | ||||||
|  | import { ChartRendererProps } from '../renderer'; | ||||||
|  | import { getSelectedFields } from '../utils'; | ||||||
|  | import { FieldOption } from './query'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * useFieldTypes | ||||||
|  |  * Get field types for using transformers | ||||||
|  |  * Only supported types will be displayed | ||||||
|  |  * Some interfaces and types will be mapped to supported types | ||||||
|  |  */ | ||||||
|  | export const useFieldTypes = (fields: FieldOption[]) => (field: any) => { | ||||||
|  |   const selectedField = field.query('.field').get('value'); | ||||||
|  |   const query = field.query('query').get('value') || {}; | ||||||
|  |   const selectedFields = getSelectedFields(fields, query); | ||||||
|  |   const fieldProps = selectedFields.find((field) => field.value === selectedField); | ||||||
|  |   const supports = Object.keys(transformers); | ||||||
|  |   field.dataSource = supports.map((key) => ({ | ||||||
|  |     label: lang(key), | ||||||
|  |     value: key, | ||||||
|  |   })); | ||||||
|  |   const map = { | ||||||
|  |     createdAt: 'datetime', | ||||||
|  |     updatedAt: 'datetime', | ||||||
|  |     double: 'number', | ||||||
|  |     integer: 'number', | ||||||
|  |     percent: 'number', | ||||||
|  |   }; | ||||||
|  |   const fieldInterface = fieldProps?.interface; | ||||||
|  |   const fieldType = fieldProps?.type; | ||||||
|  |   const key = map[fieldInterface] || map[fieldType] || fieldType; | ||||||
|  |   if (supports.includes(key)) { | ||||||
|  |     field.setState({ | ||||||
|  |       value: key, | ||||||
|  |       disabled: true, | ||||||
|  |     }); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   field.setState({ | ||||||
|  |     value: null, | ||||||
|  |     disabled: false, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const useTransformers = (field: any) => { | ||||||
|  |   const selectedType = field.query('.type').get('value'); | ||||||
|  |   if (!selectedType) { | ||||||
|  |     field.dataSource = []; | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   const options = Object.keys(transformers[selectedType] || {}).map((key) => ({ | ||||||
|  |     label: lang(key), | ||||||
|  |     value: key, | ||||||
|  |   })); | ||||||
|  |   field.dataSource = options; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const useFieldTransformer = (transform: ChartRendererProps['transform'], locale = 'en-US') => { | ||||||
|  |   return (transform || []) | ||||||
|  |     .filter((item) => item.field && item.type && item.format) | ||||||
|  |     .reduce((mp, item) => { | ||||||
|  |       const transformer = transformers[item.type][item.format]; | ||||||
|  |       if (!transformer) { | ||||||
|  |         return mp; | ||||||
|  |       } | ||||||
|  |       mp[item.field] = (val: any) => transformer(val, locale); | ||||||
|  |       return mp; | ||||||
|  |     }, {}); | ||||||
|  | }; | ||||||
| @ -0,0 +1,163 @@ | |||||||
|  | import { useMemo } from 'react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { useMemoizedFn } from 'ahooks'; | ||||||
|  | 
 | ||||||
|  | export const useDateVariable = ({ operator, 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 dateOptions = useMemo( | ||||||
|  |     () => [ | ||||||
|  |       { | ||||||
|  |         key: 'now', | ||||||
|  |         value: 'now', | ||||||
|  |         label: t('Now'), | ||||||
|  |         disabled: component !== 'DatePicker' || operatorValue === '$dateBetween', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'yesterday', | ||||||
|  |         value: 'yesterday', | ||||||
|  |         label: t('Yesterday'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'today', | ||||||
|  |         value: 'today', | ||||||
|  |         label: t('Today'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'tomorrow', | ||||||
|  |         value: 'tomorrow', | ||||||
|  |         label: t('Tomorrow'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'lastIsoWeek', | ||||||
|  |         value: 'lastIsoWeek', | ||||||
|  |         label: t('Last week'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'thisIsoWeek', | ||||||
|  |         value: 'thisIsoWeek', | ||||||
|  |         label: t('This week'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'nextIsoWeek', | ||||||
|  |         value: 'nextIsoWeek', | ||||||
|  |         label: t('Next week'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'lastMonth', | ||||||
|  |         value: 'lastMonth', | ||||||
|  |         label: t('Last month'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'thisMonth', | ||||||
|  |         value: 'thisMonth', | ||||||
|  |         label: t('This month'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'nextMonth', | ||||||
|  |         value: 'nextMonth', | ||||||
|  |         label: t('Next month'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'lastQuarter', | ||||||
|  |         value: 'lastQuarter', | ||||||
|  |         label: t('Last quarter'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'thisQuarter', | ||||||
|  |         value: 'thisQuarter', | ||||||
|  |         label: t('This quarter'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'nextQuarter', | ||||||
|  |         value: 'nextQuarter', | ||||||
|  |         label: t('Next quarter'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'lastYear', | ||||||
|  |         value: 'lastYear', | ||||||
|  |         label: t('Last year'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'thisYear', | ||||||
|  |         value: 'thisYear', | ||||||
|  |         label: t('This year'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'nextYear', | ||||||
|  |         value: 'nextYear', | ||||||
|  |         label: t('Next year'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'last7Days', | ||||||
|  |         value: 'last7Days', | ||||||
|  |         label: t('Last 7 days'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'next7Days', | ||||||
|  |         value: 'next7Days', | ||||||
|  |         label: t('Next 7 days'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'last30Days', | ||||||
|  |         value: 'last30Days', | ||||||
|  |         label: t('Last 30 days'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'next30Days', | ||||||
|  |         value: 'next30Days', | ||||||
|  |         label: t('Next 30 days'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'last90Days', | ||||||
|  |         value: 'last90Days', | ||||||
|  |         label: t('Last 90 days'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'next90Days', | ||||||
|  |         value: 'next90Days', | ||||||
|  |         label: t('Next 90 days'), | ||||||
|  |         disabled, | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |     [disabled, operatorValue, t, component], | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const result = useMemo( | ||||||
|  |     () => ({ | ||||||
|  |       label: t('Date variables'), | ||||||
|  |       value: '$nDate', | ||||||
|  |       key: '$nDate', | ||||||
|  |       disabled: dateOptions.every((option) => option.disabled), | ||||||
|  |       children: dateOptions, | ||||||
|  |     }), | ||||||
|  |     [dateOptions, t], | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   if (!operator || !schema) return null; | ||||||
|  | 
 | ||||||
|  |   return result; | ||||||
|  | }; | ||||||
| @ -0,0 +1,40 @@ | |||||||
|  | import { useCallback, useMemo } from 'react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { Schema } from '@formily/react'; | ||||||
|  | import { useCollectionFieldsOptions } from '@nocobase/client'; | ||||||
|  | import { useMemoizedFn } from 'ahooks'; | ||||||
|  | 
 | ||||||
|  | export const useUserVariable = ({ schema }: { schema: any }) => { | ||||||
|  |   const { t: trans } = useTranslation(); | ||||||
|  |   const t = useMemoizedFn(trans); | ||||||
|  |   const options = useCollectionFieldsOptions('users'); | ||||||
|  |   const component = schema?.['x-component']; | ||||||
|  |   const getOptions = useCallback( | ||||||
|  |     (options: any) => | ||||||
|  |       options.map((option: any) => { | ||||||
|  |         const result = { | ||||||
|  |           key: option.name, | ||||||
|  |           value: option.name, | ||||||
|  |           disabled: !option.children && component !== option.schema?.['x-component'], | ||||||
|  |           label: Schema.compile(option.title, { t }), | ||||||
|  |         }; | ||||||
|  |         if (option.children) { | ||||||
|  |           result['children'] = getOptions(option.children); | ||||||
|  |         } | ||||||
|  |         return result; | ||||||
|  |       }), | ||||||
|  |     [t, component], | ||||||
|  |   ); | ||||||
|  |   const children = useMemo(() => getOptions(options), [getOptions, options]); | ||||||
|  |   const result = useMemo( | ||||||
|  |     () => ({ | ||||||
|  |       label: t('Current user'), | ||||||
|  |       value: '$user', | ||||||
|  |       key: '$user', | ||||||
|  |       children, | ||||||
|  |     }), | ||||||
|  |     [children, t], | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   return result; | ||||||
|  | }; | ||||||
| @ -0,0 +1,17 @@ | |||||||
|  | import { useField } from '@formily/react'; | ||||||
|  | import { useMemo } from 'react'; | ||||||
|  | import { useDateVariable } from './useDateVariable'; | ||||||
|  | import { useUserVariable } from './useUserVariable'; | ||||||
|  | 
 | ||||||
|  | export const useVariableOptions = () => { | ||||||
|  |   const field = useField<any>(); | ||||||
|  |   const { operator, schema } = field.data || {}; | ||||||
|  |   const userVariable = useUserVariable({ schema }); | ||||||
|  |   const dateVariable = useDateVariable({ operator, schema }); | ||||||
|  | 
 | ||||||
|  |   const result = useMemo(() => [userVariable, dateVariable].filter(Boolean), [dateVariable, userVariable]); | ||||||
|  | 
 | ||||||
|  |   if (!operator || !schema) return []; | ||||||
|  | 
 | ||||||
|  |   return result; | ||||||
|  | }; | ||||||
| @ -10,7 +10,7 @@ import { | |||||||
| import { Empty, Result, Spin, Typography } from 'antd'; | import { Empty, Result, Spin, Typography } from 'antd'; | ||||||
| import React, { useContext } from 'react'; | import React, { useContext } from 'react'; | ||||||
| import { ErrorBoundary } from 'react-error-boundary'; | import { ErrorBoundary } from 'react-error-boundary'; | ||||||
| import { ChartConfigContext } from '../block'; | import { ChartConfigContext } from '../configure/ChartConfigure'; | ||||||
| import { useData, useFieldTransformer, useFieldsWithAssociation } from '../hooks'; | import { useData, useFieldTransformer, useFieldsWithAssociation } from '../hooks'; | ||||||
| import { useChartsTranslation } from '../locale'; | import { useChartsTranslation } from '../locale'; | ||||||
| import { createRendererSchema, getField } from '../utils'; | import { createRendererSchema, getField } from '../utils'; | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { Schema } from '@formily/react'; | import { Schema } from '@formily/react'; | ||||||
| import { uid } from '@formily/shared'; | import { uid } from '@formily/shared'; | ||||||
| import { SelectedField } from './block'; | import { SelectedField } from './configure/ChartConfigure'; | ||||||
| import { FieldOption } from './hooks'; | import { FieldOption } from './hooks'; | ||||||
| import { QueryProps } from './renderer'; | import { QueryProps } from './renderer'; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| import { Database } from '@nocobase/database'; | import { Database } from '@nocobase/database'; | ||||||
| import { MockServer, mockServer } from '@nocobase/test'; | import { MockServer, mockServer } from '@nocobase/test'; | ||||||
| import { queryData } from '../actions/query'; | import { parseBuilder, parseFieldAndAssociations, queryData } from '../actions/query'; | ||||||
| import ChartsV2Plugin from '../plugin'; | import ChartsV2Plugin from '../plugin'; | ||||||
|  | import compose from 'koa-compose'; | ||||||
| 
 | 
 | ||||||
| describe('api', () => { | describe('api', () => { | ||||||
|   let app: MockServer; |   let app: MockServer; | ||||||
| @ -52,51 +53,67 @@ describe('api', () => { | |||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   test('query', async () => { |   test('query', async () => { | ||||||
|     const result = await queryData({ db } as any, { |     const ctx = { | ||||||
|       collection: 'chart_test', |       db, | ||||||
|       measures: [ |       action: { | ||||||
|         { |         params: { | ||||||
|           field: ['price'], |           values: { | ||||||
|           alias: 'Price', |             collection: 'chart_test', | ||||||
|  |             measures: [ | ||||||
|  |               { | ||||||
|  |                 field: ['price'], | ||||||
|  |                 alias: 'Price', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 field: ['count'], | ||||||
|  |                 alias: 'Count', | ||||||
|  |               }, | ||||||
|  |             ], | ||||||
|  |             dimensions: [ | ||||||
|  |               { | ||||||
|  |                 field: ['title'], | ||||||
|  |                 alias: 'Title', | ||||||
|  |               }, | ||||||
|  |             ], | ||||||
|  |           }, | ||||||
|         }, |         }, | ||||||
|         { |       }, | ||||||
|           field: ['count'], |     } as any; | ||||||
|           alias: 'Count', |     await compose([parseFieldAndAssociations, parseBuilder, queryData])(ctx, async () => {}); | ||||||
|         }, |     expect(ctx.action.params.values.data).toBeDefined(); | ||||||
|       ], |  | ||||||
|       dimensions: [ |  | ||||||
|         { |  | ||||||
|           field: ['title'], |  | ||||||
|           alias: 'Title', |  | ||||||
|         }, |  | ||||||
|       ], |  | ||||||
|     }); |  | ||||||
|     expect(result).toBeDefined(); |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   test('query with sort', async () => { |   test('query with sort', async () => { | ||||||
|     const result = await queryData({ db } as any, { |     const ctx = { | ||||||
|       collection: 'chart_test', |       db, | ||||||
|       measures: [ |       action: { | ||||||
|         { |         params: { | ||||||
|           field: ['price'], |           values: { | ||||||
|           aggregation: 'sum', |             collection: 'chart_test', | ||||||
|           alias: 'Price', |             measures: [ | ||||||
|  |               { | ||||||
|  |                 field: ['price'], | ||||||
|  |                 aggregation: 'sum', | ||||||
|  |                 alias: 'Price', | ||||||
|  |               }, | ||||||
|  |             ], | ||||||
|  |             dimensions: [ | ||||||
|  |               { | ||||||
|  |                 field: ['title'], | ||||||
|  |                 alias: 'Title', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 field: ['createdAt'], | ||||||
|  |                 format: 'YYYY-MM', | ||||||
|  |               }, | ||||||
|  |             ], | ||||||
|  |             orders: [{ field: 'createdAt', order: 'asc' }], | ||||||
|  |           }, | ||||||
|         }, |         }, | ||||||
|       ], |       }, | ||||||
|       dimensions: [ |     } as any; | ||||||
|         { |     await compose([parseFieldAndAssociations, parseBuilder, queryData])(ctx, async () => {}); | ||||||
|           field: ['title'], |     expect(ctx.action.params.values.data).toBeDefined(); | ||||||
|           alias: 'Title', |     expect(ctx.action.params.values.data).toMatchObject([{ createdAt: '2023-01' }, { createdAt: '2023-02' }]); | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           field: ['createdAt'], |  | ||||||
|           format: 'YYYY-MM', |  | ||||||
|         }, |  | ||||||
|       ], |  | ||||||
|       orders: [{ field: 'createdAt', order: 'asc' }], |  | ||||||
|     }); |  | ||||||
|     expect(result).toBeDefined(); |  | ||||||
|     expect(result).toMatchObject([{ createdAt: '2023-01' }, { createdAt: '2023-02' }]); |  | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import { MockServer, mockServer } from '@nocobase/test'; | import { MockServer, mockServer } from '@nocobase/test'; | ||||||
| import * as formatter from '../actions/formatter'; | import * as formatter from '../actions/formatter'; | ||||||
| import { cacheWrap, parseBuilder, parseFieldAndAssociations } from '../actions/query'; | import { cacheMiddleware, parseBuilder, parseFieldAndAssociations } from '../actions/query'; | ||||||
|  | import compose from 'koa-compose'; | ||||||
| 
 | 
 | ||||||
| describe('query', () => { | describe('query', () => { | ||||||
|   describe('parseBuilder', () => { |   describe('parseBuilder', () => { | ||||||
| @ -51,6 +52,7 @@ describe('query', () => { | |||||||
|         ], |         ], | ||||||
|       }); |       }); | ||||||
|       ctx = { |       ctx = { | ||||||
|  |         app, | ||||||
|         db: { |         db: { | ||||||
|           sequelize, |           sequelize, | ||||||
|           getRepository: (name: string) => app.db.getRepository(name), |           getRepository: (name: string) => app.db.getRepository(name), | ||||||
| @ -63,13 +65,21 @@ describe('query', () => { | |||||||
|       }; |       }; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should parse field and associations', () => { |     it('should parse field and associations', async () => { | ||||||
|       const associations = parseFieldAndAssociations(ctx, { |       const context = { | ||||||
|         collection: 'orders', |         ...ctx, | ||||||
|         measures: [{ field: ['price'], aggregation: 'sum', alias: 'price' }], |         action: { | ||||||
|         dimensions: [{ field: ['createdAt'] }, { field: ['user', 'name'] }], |           params: { | ||||||
|       }); |             values: { | ||||||
|       expect(associations).toMatchObject({ |               collection: 'orders', | ||||||
|  |               measures: [{ field: ['price'], aggregation: 'sum', alias: 'price' }], | ||||||
|  |               dimensions: [{ field: ['createdAt'] }, { field: ['user', 'name'] }], | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |       await parseFieldAndAssociations(context, async () => {}); | ||||||
|  |       expect(context.action.params.values).toMatchObject({ | ||||||
|         measures: [{ field: 'orders.price', aggregation: 'sum', alias: 'price', type: 'double' }], |         measures: [{ field: 'orders.price', aggregation: 'sum', alias: 'price', type: 'double' }], | ||||||
|         dimensions: [ |         dimensions: [ | ||||||
|           { field: 'orders.created_at', alias: 'createdAt', type: 'date' }, |           { field: 'orders.created_at', alias: 'createdAt', type: 'date' }, | ||||||
| @ -79,14 +89,22 @@ describe('query', () => { | |||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should parse measures', () => { |     it('should parse measures', async () => { | ||||||
|       const measures1 = [ |       const measures1 = [ | ||||||
|         { |         { | ||||||
|           field: ['price'], |           field: ['price'], | ||||||
|         }, |         }, | ||||||
|       ]; |       ]; | ||||||
|       const { queryParams: result1 } = parseBuilder(ctx, { collection: 'orders', measures: measures1 }); |       const context = { | ||||||
|       expect(result1.attributes).toEqual([['orders.price', 'price']]); |         ...ctx, | ||||||
|  |         action: { | ||||||
|  |           params: { | ||||||
|  |             values: { collection: 'orders', measures: measures1 }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |       await compose([parseFieldAndAssociations, parseBuilder])(context, async () => {}); | ||||||
|  |       expect(context.action.params.values.queryParams.attributes).toEqual([['orders.price', 'price']]); | ||||||
| 
 | 
 | ||||||
|       const measures2 = [ |       const measures2 = [ | ||||||
|         { |         { | ||||||
| @ -95,11 +113,19 @@ describe('query', () => { | |||||||
|           alias: 'price-alias', |           alias: 'price-alias', | ||||||
|         }, |         }, | ||||||
|       ]; |       ]; | ||||||
|       const { queryParams: result2 } = parseBuilder(ctx, { collection: 'orders', measures: measures2 }); |       const context2 = { | ||||||
|       expect(result2.attributes).toEqual([[['sum', 'orders.price'], 'price-alias']]); |         ...ctx, | ||||||
|  |         action: { | ||||||
|  |           params: { | ||||||
|  |             values: { collection: 'orders', measures: measures2 }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |       await compose([parseFieldAndAssociations, parseBuilder])(context2, async () => {}); | ||||||
|  |       expect(context2.action.params.values.queryParams.attributes).toEqual([[['sum', 'orders.price'], 'price-alias']]); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should parse dimensions', () => { |     it('should parse dimensions', async () => { | ||||||
|       jest.spyOn(formatter, 'formatter').mockReturnValue('formatted-field'); |       jest.spyOn(formatter, 'formatter').mockReturnValue('formatted-field'); | ||||||
|       const dimensions = [ |       const dimensions = [ | ||||||
|         { |         { | ||||||
| @ -108,9 +134,17 @@ describe('query', () => { | |||||||
|           alias: 'Created at', |           alias: 'Created at', | ||||||
|         }, |         }, | ||||||
|       ]; |       ]; | ||||||
|       const { queryParams: result } = parseBuilder(ctx, { collection: 'orders', dimensions }); |       const context = { | ||||||
|       expect(result.attributes).toEqual([['formatted-field', 'Created at']]); |         ...ctx, | ||||||
|       expect(result.group).toEqual([]); |         action: { | ||||||
|  |           params: { | ||||||
|  |             values: { collection: 'orders', dimensions }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |       await compose([parseFieldAndAssociations, parseBuilder])(context, async () => {}); | ||||||
|  |       expect(context.action.params.values.queryParams.attributes).toEqual([['formatted-field', 'Created at']]); | ||||||
|  |       expect(context.action.params.values.queryParams.group).toEqual([]); | ||||||
| 
 | 
 | ||||||
|       const measures = [ |       const measures = [ | ||||||
|         { |         { | ||||||
| @ -118,30 +152,46 @@ describe('query', () => { | |||||||
|           aggregation: 'sum', |           aggregation: 'sum', | ||||||
|         }, |         }, | ||||||
|       ]; |       ]; | ||||||
|       const { queryParams: result2 } = parseBuilder(ctx, { collection: 'orders', measures, dimensions }); |       const context2 = { | ||||||
|       expect(result2.group).toEqual(['formatted-field']); |         ...ctx, | ||||||
|  |         action: { | ||||||
|  |           params: { | ||||||
|  |             values: { collection: 'orders', measures, dimensions }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |       await compose([parseFieldAndAssociations, parseBuilder])(context2, async () => {}); | ||||||
|  |       expect(context2.action.params.values.queryParams.group).toEqual(['formatted-field']); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should parse filter', () => { |     it('should parse filter', async () => { | ||||||
|       const filter = { |       const filter = { | ||||||
|         createdAt: { |         createdAt: { | ||||||
|           $gt: '2020-01-01', |           $gt: '2020-01-01', | ||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|       const { queryParams: result } = parseBuilder(ctx, { collection: 'orders', filter }); |       const context = { | ||||||
|       expect(result.where.createdAt).toBeDefined(); |         ...ctx, | ||||||
|  |         action: { | ||||||
|  |           params: { | ||||||
|  |             values: { collection: 'orders', filter }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |       await compose([parseFieldAndAssociations, parseBuilder])(context, async () => {}); | ||||||
|  |       expect(context.action.params.values.queryParams.where.createdAt).toBeDefined(); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('cacheWrap', () => { |   describe('cacheMiddleware', () => { | ||||||
|     const key = 'test-key'; |     const key = 'test-key'; | ||||||
|     const value = 'test-val'; |     const value = 'test-val'; | ||||||
|  |     const query = jest.fn().mockImplementation(async (ctx, next) => { | ||||||
|  |       ctx.body = value; | ||||||
|  |       await next(); | ||||||
|  |     }); | ||||||
|     class MockCache { |     class MockCache { | ||||||
|       map: Map<string, any> = new Map(); |       map: Map<string, any> = new Map(); | ||||||
|       async func() { |  | ||||||
|         return value; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       get(key: string) { |       get(key: string) { | ||||||
|         return this.map.get(key); |         return this.map.get(key); | ||||||
|       } |       } | ||||||
| @ -149,72 +199,74 @@ describe('query', () => { | |||||||
|         this.map.set(key, value); |         this.map.set(key, value); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     let cache: any; |     let ctx: any; | ||||||
|     let query: () => Promise<any>; |  | ||||||
| 
 |  | ||||||
|     beforeEach(() => { |     beforeEach(() => { | ||||||
|       cache = new MockCache(); |       const cache = new MockCache(); | ||||||
|  |       ctx = { | ||||||
|  |         app: { | ||||||
|  |           getPlugin: () => ({ cache }), | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should use cache', async () => { |     it('should use cache', async () => { | ||||||
|       query = async () => |       const context = { | ||||||
|         await cacheWrap(cache, { |         ...ctx, | ||||||
|           key, |         action: { | ||||||
|           func: cache.func, |           params: { | ||||||
|           useCache: true, |             values: { cache: { enabled: true }, refresh: false, uid: key }, | ||||||
|           refresh: false, |           }, | ||||||
|         }); |         }, | ||||||
| 
 |       }; | ||||||
|       const spy = jest.spyOn(cache, 'func'); |       const cache = context.app.getPlugin().cache; | ||||||
|       expect(cache.get(key)).toBeUndefined(); |       expect(cache.get(key)).toBeUndefined(); | ||||||
|       const result = await query(); |       await compose([cacheMiddleware, query])(context, async () => {}); | ||||||
|       expect(cache.func).toBeCalled(); |       expect(query).toBeCalled(); | ||||||
|       expect(result).toEqual(value); |       expect(context.body).toEqual(value); | ||||||
|       expect(cache.get(key)).toEqual(value); |       expect(cache.get(key)).toEqual(value); | ||||||
| 
 | 
 | ||||||
|       spy.mockReset(); |       jest.clearAllMocks(); | ||||||
|       const result2 = await query(); |       await compose([cacheMiddleware, query])(context, async () => {}); | ||||||
|       expect(result2).toEqual(value); |       expect(context.body).toEqual(value); | ||||||
|       expect(cache.func).not.toBeCalled(); |       expect(query).not.toBeCalled(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should not use cache', async () => { |     it('should not use cache', async () => { | ||||||
|       query = async () => |       const context = { | ||||||
|         await cacheWrap(cache, { |         ...ctx, | ||||||
|           key, |         action: { | ||||||
|           func: cache.func, |           params: { | ||||||
|           useCache: false, |             values: { uid: key }, | ||||||
|           refresh: false, |           }, | ||||||
|         }); |         }, | ||||||
| 
 |       }; | ||||||
|  |       const cache = context.app.getPlugin().cache; | ||||||
|       cache.set(key, value); |       cache.set(key, value); | ||||||
|       expect(cache.get(key)).toBeDefined(); |       expect(cache.get(key)).toBeDefined(); | ||||||
|       jest.spyOn(cache, 'func'); |       await compose([cacheMiddleware, query])(context, async () => {}); | ||||||
|       const result = await query(); |       expect(query).toBeCalled(); | ||||||
|       expect(cache.func).toBeCalled(); |       expect(context.body).toEqual(value); | ||||||
|       expect(result).toEqual(value); |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should refresh', async () => { |     it('should refresh', async () => { | ||||||
|       query = async () => |       const context = { | ||||||
|         await cacheWrap(cache, { |         ...ctx, | ||||||
|           key, |         action: { | ||||||
|           func: cache.func, |           params: { | ||||||
|           useCache: true, |             values: { cache: { enabled: true }, refresh: true, uid: key }, | ||||||
|           refresh: true, |           }, | ||||||
|         }); |         }, | ||||||
| 
 |       }; | ||||||
|       const spy = jest.spyOn(cache, 'func'); |       const cache = context.app.getPlugin().cache; | ||||||
|       expect(cache.get(key)).toBeUndefined(); |       expect(cache.get(key)).toBeUndefined(); | ||||||
|       const result = await query(); |       await compose([cacheMiddleware, query])(context, async () => {}); | ||||||
|       expect(cache.func).toBeCalled(); |       expect(query).toBeCalled(); | ||||||
|       expect(result).toEqual(value); |       expect(context.body).toEqual(value); | ||||||
|       expect(cache.get(key)).toEqual(value); |       expect(cache.get(key)).toEqual(value); | ||||||
| 
 | 
 | ||||||
|       spy.mockClear(); |       await compose([cacheMiddleware, query])(context, async () => {}); | ||||||
|       const result2 = await query(); |       expect(query).toBeCalled(); | ||||||
|       expect(cache.func).toBeCalled(); |       expect(context.body).toEqual(value); | ||||||
|       expect(result2).toEqual(value); |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -1,8 +1,9 @@ | |||||||
| import { Context, Next } from '@nocobase/actions'; | import { Context, Next } from '@nocobase/actions'; | ||||||
| import { Cache } from '@nocobase/cache'; |  | ||||||
| import { Field, FilterParser, snakeCase } from '@nocobase/database'; | import { Field, FilterParser, snakeCase } from '@nocobase/database'; | ||||||
| import ChartsV2Plugin from '../plugin'; | import ChartsV2Plugin from '../plugin'; | ||||||
| import { formatter } from './formatter'; | import { formatter } from './formatter'; | ||||||
|  | import compose from 'koa-compose'; | ||||||
|  | import { parseFilter, getDateVars } from '@nocobase/utils'; | ||||||
| 
 | 
 | ||||||
| type MeasureProps = { | type MeasureProps = { | ||||||
|   field: string | string[]; |   field: string | string[]; | ||||||
| @ -44,8 +45,126 @@ type QueryParams = Partial<{ | |||||||
|   refresh: boolean; |   refresh: boolean; | ||||||
| }>; | }>; | ||||||
| 
 | 
 | ||||||
| export const parseFieldAndAssociations = (ctx: Context, params: QueryParams) => { | export const postProcess = async (ctx: Context, next: Next) => { | ||||||
|   const { collection: collectionName, measures, dimensions, orders, filter } = params; |   const { sequelize } = ctx.db; | ||||||
|  |   const dialect = sequelize.getDialect(); | ||||||
|  |   const { data, fieldMap } = ctx.action.params.values as { | ||||||
|  |     data: any[]; | ||||||
|  |     fieldMap: { [source: string]: { type?: string } }; | ||||||
|  |   }; | ||||||
|  |   switch (dialect) { | ||||||
|  |     case 'postgres': | ||||||
|  |       // https://github.com/sequelize/sequelize/issues/4550
 | ||||||
|  |       ctx.body = data.map((record) => { | ||||||
|  |         const result = {}; | ||||||
|  |         Object.entries(record).forEach(([key, value]) => { | ||||||
|  |           const { type } = fieldMap[key] || {}; | ||||||
|  |           switch (type) { | ||||||
|  |             case 'bigInt': | ||||||
|  |             case 'integer': | ||||||
|  |             case 'float': | ||||||
|  |             case 'double': | ||||||
|  |               value = Number(value); | ||||||
|  |               break; | ||||||
|  |           } | ||||||
|  |           result[key] = value; | ||||||
|  |         }); | ||||||
|  |         return result; | ||||||
|  |       }); | ||||||
|  |       break; | ||||||
|  |     default: | ||||||
|  |       ctx.body = data; | ||||||
|  |   } | ||||||
|  |   await next(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const queryData = async (ctx: Context, next: Next) => { | ||||||
|  |   const { collection, queryParams, fieldMap } = ctx.action.params.values; | ||||||
|  |   const model = ctx.db.getModel(collection); | ||||||
|  |   const data = await model.findAll(queryParams); | ||||||
|  |   ctx.action.params.values = { | ||||||
|  |     data, | ||||||
|  |     fieldMap, | ||||||
|  |   }; | ||||||
|  |   await next(); | ||||||
|  |   // if (!sql) {
 | ||||||
|  |   //   return await repository.find(parseBuilder(ctx, { collection, measures, dimensions, orders, filter, limit }));
 | ||||||
|  |   // }
 | ||||||
|  | 
 | ||||||
|  |   // const statement = `SELECT ${sql.fields} FROM ${collection} ${sql.clauses}`;
 | ||||||
|  |   // const [data] = await ctx.db.sequelize.query(statement);
 | ||||||
|  |   // return data;
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const parseBuilder = async (ctx: Context, next: Next) => { | ||||||
|  |   const { sequelize } = ctx.db; | ||||||
|  |   const { measures, dimensions, orders, include, where, limit } = ctx.action.params.values; | ||||||
|  |   const attributes = []; | ||||||
|  |   const group = []; | ||||||
|  |   const order = []; | ||||||
|  |   const fieldMap = {}; | ||||||
|  |   let hasAgg = false; | ||||||
|  | 
 | ||||||
|  |   measures.forEach((measure: MeasureProps & { field: string }) => { | ||||||
|  |     const { field, aggregation, alias } = measure; | ||||||
|  |     const attribute = []; | ||||||
|  |     const col = sequelize.col(field); | ||||||
|  |     if (aggregation) { | ||||||
|  |       hasAgg = true; | ||||||
|  |       attribute.push(sequelize.fn(aggregation, col)); | ||||||
|  |     } else { | ||||||
|  |       attribute.push(col); | ||||||
|  |     } | ||||||
|  |     if (alias) { | ||||||
|  |       attribute.push(alias); | ||||||
|  |     } | ||||||
|  |     attributes.push(attribute.length > 1 ? attribute : attribute[0]); | ||||||
|  |     fieldMap[alias || field] = measure; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   dimensions.forEach((dimension: DimensionProps & { field: string }) => { | ||||||
|  |     const { field, format, alias, type } = dimension; | ||||||
|  |     const attribute = []; | ||||||
|  |     const col = sequelize.col(field); | ||||||
|  |     if (format) { | ||||||
|  |       attribute.push(formatter(sequelize, type, field, format)); | ||||||
|  |     } else { | ||||||
|  |       attribute.push(col); | ||||||
|  |     } | ||||||
|  |     if (alias) { | ||||||
|  |       attribute.push(alias); | ||||||
|  |     } | ||||||
|  |     attributes.push(attribute.length > 1 ? attribute : attribute[0]); | ||||||
|  |     if (hasAgg) { | ||||||
|  |       group.push(attribute[0]); | ||||||
|  |     } | ||||||
|  |     fieldMap[alias || field] = dimension; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   orders.forEach((item: OrderProps) => { | ||||||
|  |     const alias = sequelize.getQueryInterface().quoteIdentifier(item.alias); | ||||||
|  |     const name = hasAgg ? sequelize.literal(alias) : sequelize.col(item.field as string); | ||||||
|  |     order.push([name, item.order || 'ASC']); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   ctx.action.params.values = { | ||||||
|  |     ...ctx.action.params.values, | ||||||
|  |     queryParams: { | ||||||
|  |       where, | ||||||
|  |       attributes, | ||||||
|  |       include, | ||||||
|  |       group, | ||||||
|  |       order, | ||||||
|  |       limit: limit > 2000 ? 2000 : limit, | ||||||
|  |       raw: true, | ||||||
|  |     }, | ||||||
|  |     fieldMap, | ||||||
|  |   }; | ||||||
|  |   await next(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const parseFieldAndAssociations = async (ctx: Context, next: Next) => { | ||||||
|  |   const { collection: collectionName, measures, dimensions, orders, filter } = ctx.action.params.values as QueryParams; | ||||||
|   const collection = ctx.db.getCollection(collectionName); |   const collection = ctx.db.getCollection(collectionName); | ||||||
|   const fields = collection.fields; |   const fields = collection.fields; | ||||||
|   const underscored = ctx.db.options.underscored; |   const underscored = ctx.db.options.underscored; | ||||||
| @ -109,182 +228,111 @@ export const parseFieldAndAssociations = (ctx: Context, params: QueryParams) => | |||||||
|     return item; |     return item; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   return { |   ctx.action.params.values = { | ||||||
|  |     ...ctx.action.params.values, | ||||||
|     where, |     where, | ||||||
|     measures: parsedMeasures, |     measures: parsedMeasures, | ||||||
|     dimensions: parsedDimensions, |     dimensions: parsedDimensions, | ||||||
|     orders: parsedOrders, |     orders: parsedOrders, | ||||||
|     include: [...include, ...(parsedFilterInclude || [])], |     include: [...include, ...(parsedFilterInclude || [])], | ||||||
|   }; |   }; | ||||||
|  |   await next(); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const parseBuilder = (ctx: Context, builder: QueryParams) => { | export const parseVariables = async (ctx: Context, next: Next) => { | ||||||
|   const { sequelize } = ctx.db; |   const { filter } = ctx.action.params.values; | ||||||
|   const { limit } = builder; |   if (!filter) { | ||||||
|   const { measures, dimensions, orders, include, where } = parseFieldAndAssociations(ctx, builder); |     return next(); | ||||||
|   const attributes = []; |   } | ||||||
|   const group = []; |   const isNumeric = (str: any) => { | ||||||
|   const order = []; |     if (typeof str === 'number') return true; | ||||||
|   const fieldMap = {}; |     if (typeof str != 'string') return false; | ||||||
|   let hasAgg = false; |     return !isNaN(str as any) && !isNaN(parseFloat(str)); | ||||||
| 
 |  | ||||||
|   measures.forEach((measure: MeasureProps & { field: string }) => { |  | ||||||
|     const { field, aggregation, alias } = measure; |  | ||||||
|     const attribute = []; |  | ||||||
|     const col = sequelize.col(field); |  | ||||||
|     if (aggregation) { |  | ||||||
|       hasAgg = true; |  | ||||||
|       attribute.push(sequelize.fn(aggregation, col)); |  | ||||||
|     } else { |  | ||||||
|       attribute.push(col); |  | ||||||
|     } |  | ||||||
|     if (alias) { |  | ||||||
|       attribute.push(alias); |  | ||||||
|     } |  | ||||||
|     attributes.push(attribute.length > 1 ? attribute : attribute[0]); |  | ||||||
|     fieldMap[alias || field] = measure; |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   dimensions.forEach((dimension: DimensionProps & { field: string }) => { |  | ||||||
|     const { field, format, alias, type } = dimension; |  | ||||||
|     const attribute = []; |  | ||||||
|     const col = sequelize.col(field); |  | ||||||
|     if (format) { |  | ||||||
|       attribute.push(formatter(sequelize, type, field, format)); |  | ||||||
|     } else { |  | ||||||
|       attribute.push(col); |  | ||||||
|     } |  | ||||||
|     if (alias) { |  | ||||||
|       attribute.push(alias); |  | ||||||
|     } |  | ||||||
|     attributes.push(attribute.length > 1 ? attribute : attribute[0]); |  | ||||||
|     if (hasAgg) { |  | ||||||
|       group.push(attribute[0]); |  | ||||||
|     } |  | ||||||
|     fieldMap[alias || field] = dimension; |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   orders.forEach((item: OrderProps) => { |  | ||||||
|     const alias = sequelize.getQueryInterface().quoteIdentifier(item.alias); |  | ||||||
|     const name = hasAgg ? sequelize.literal(alias) : sequelize.col(item.field as string); |  | ||||||
|     order.push([name, item.order || 'ASC']); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     queryParams: { |  | ||||||
|       where, |  | ||||||
|       attributes, |  | ||||||
|       include, |  | ||||||
|       group, |  | ||||||
|       order, |  | ||||||
|       limit: limit > 2000 ? 2000 : limit, |  | ||||||
|       raw: true, |  | ||||||
|     }, |  | ||||||
|     fieldMap, |  | ||||||
|   }; |   }; | ||||||
| }; |   const getUser = () => { | ||||||
| 
 |     return async ({ fields }) => { | ||||||
| export const processData = (ctx: Context, data: any[], fieldMap: { [source: string]: { type?: string } }) => { |       const userFields = fields.filter((f) => f && ctx.db.getFieldByPath('users.' + f)); | ||||||
|   const { sequelize } = ctx.db; |       ctx.logger?.info('filter-parse: ', { userFields }); | ||||||
|   const dialect = sequelize.getDialect(); |       if (!ctx.state.currentUser) { | ||||||
|   switch (dialect) { |         return; | ||||||
|     case 'postgres': |       } | ||||||
|       // https://github.com/sequelize/sequelize/issues/4550
 |       if (!userFields.length) { | ||||||
|       return data.map((record) => { |         return; | ||||||
|         const result = {}; |       } | ||||||
|         Object.entries(record).forEach(([key, value]) => { |       const user = await ctx.db.getRepository('users').findOne({ | ||||||
|           const { type } = fieldMap[key] || {}; |         filterByTk: ctx.state.currentUser.id, | ||||||
|           switch (type) { |         fields: userFields, | ||||||
|             case 'bigInt': |  | ||||||
|             case 'integer': |  | ||||||
|             case 'float': |  | ||||||
|             case 'double': |  | ||||||
|               value = Number(value); |  | ||||||
|               break; |  | ||||||
|           } |  | ||||||
|           result[key] = value; |  | ||||||
|         }); |  | ||||||
|         return result; |  | ||||||
|       }); |       }); | ||||||
|     default: |       ctx.logger?.info('filter-parse: ', { | ||||||
|       return data; |         $user: user?.toJSON(), | ||||||
|   } |       }); | ||||||
|  |       return user; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  |   ctx.action.params.values.filter = await parseFilter(filter, { | ||||||
|  |     timezone: ctx.get('x-timezone'), | ||||||
|  |     now: new Date().toISOString(), | ||||||
|  |     getField: (path: string) => { | ||||||
|  |       const fieldPath = path | ||||||
|  |         .split('.') | ||||||
|  |         .filter((p) => !p.startsWith('$') && !isNumeric(p)) | ||||||
|  |         .join('.'); | ||||||
|  |       const { resourceName } = ctx.action; | ||||||
|  |       return ctx.db.getFieldByPath(`${resourceName}.${fieldPath}`); | ||||||
|  |     }, | ||||||
|  |     vars: { | ||||||
|  |       $nDate: getDateVars(), | ||||||
|  |       $user: getUser(), | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  |   await next(); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const queryData = async (ctx: Context, builder: QueryParams) => { | export const cacheMiddleware = async (ctx: Context, next: Next) => { | ||||||
|   const { collection, measures, dimensions, orders, filter, limit, sql } = builder; |   const { uid, cache: cacheConfig, refresh } = ctx.action.params.values as QueryParams; | ||||||
|   const model = ctx.db.getModel(collection); |   const plugin = ctx.app.getPlugin('data-visualization') as ChartsV2Plugin; | ||||||
|   const { queryParams, fieldMap } = parseBuilder(ctx, { collection, measures, dimensions, orders, filter, limit }); |   const cache = plugin.cache; | ||||||
|   const data = await model.findAll(queryParams); |   const useCache = cacheConfig?.enabled && uid; | ||||||
|   return processData(ctx, data, fieldMap); |  | ||||||
|   // if (!sql) {
 |  | ||||||
|   //   return await repository.find(parseBuilder(ctx, { collection, measures, dimensions, orders, filter, limit }));
 |  | ||||||
|   // }
 |  | ||||||
| 
 | 
 | ||||||
|   // const statement = `SELECT ${sql.fields} FROM ${collection} ${sql.clauses}`;
 |  | ||||||
|   // const [data] = await ctx.db.sequelize.query(statement);
 |  | ||||||
|   // return data;
 |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const cacheWrap = async ( |  | ||||||
|   cache: Cache, |  | ||||||
|   options: { |  | ||||||
|     func: () => Promise<any>; |  | ||||||
|     key: string; |  | ||||||
|     ttl?: number; |  | ||||||
|     useCache?: boolean; |  | ||||||
|     refresh?: boolean; |  | ||||||
|   }, |  | ||||||
| ) => { |  | ||||||
|   const { func, key, ttl, useCache, refresh } = options; |  | ||||||
|   if (useCache && !refresh) { |   if (useCache && !refresh) { | ||||||
|     const data = await cache.get(key); |     const data = await cache.get(uid); | ||||||
|     if (data) { |     if (data) { | ||||||
|       return data; |       ctx.body = data; | ||||||
|  |       return; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   const data = await func(); |   await next(); | ||||||
|   if (useCache) { |   if (useCache) { | ||||||
|     await cache.set(key, data, ttl); |     console.log(uid, ctx.body); | ||||||
|  |     await cache.set(uid, ctx.body, cacheConfig?.ttl || 30); | ||||||
|  |     console.log(cache.get(uid)); | ||||||
|   } |   } | ||||||
|   return data; |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const query = async (ctx: Context, next: Next) => { | const checkPermission = (ctx: Context, next: Next) => { | ||||||
|   const { |   const { collection } = ctx.action.params.values as QueryParams; | ||||||
|     uid, |  | ||||||
|     collection, |  | ||||||
|     measures, |  | ||||||
|     dimensions, |  | ||||||
|     orders, |  | ||||||
|     filter, |  | ||||||
|     limit, |  | ||||||
|     sql, |  | ||||||
|     cache: cacheConfig, |  | ||||||
|     refresh, |  | ||||||
|   } = ctx.action.params.values as QueryParams; |  | ||||||
|   const roleName = ctx.state.currentRole || 'anonymous'; |   const roleName = ctx.state.currentRole || 'anonymous'; | ||||||
|   const can = ctx.app.acl.can({ role: roleName, resource: collection, action: 'list' }); |   const can = ctx.app.acl.can({ role: roleName, resource: collection, action: 'list' }); | ||||||
|   if (!can && roleName !== 'root') { |   if (!can && roleName !== 'root') { | ||||||
|     ctx.throw(403, 'No permissions'); |     ctx.throw(403, 'No permissions'); | ||||||
|   } |   } | ||||||
|  |   return next(); | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
|   const plugin = ctx.app.getPlugin('data-visualization') as ChartsV2Plugin; | export const query = async (ctx: Context, next: Next) => { | ||||||
|   const cache = plugin.cache; |  | ||||||
|   const useCache = cacheConfig?.enabled && uid; |  | ||||||
| 
 |  | ||||||
|   try { |   try { | ||||||
|     ctx.body = await cacheWrap(cache, { |     await compose([ | ||||||
|       func: async () => await queryData(ctx, { collection, measures, dimensions, orders, filter, limit, sql }), |       checkPermission, | ||||||
|       key: uid, |       cacheMiddleware, | ||||||
|       ttl: cacheConfig?.ttl || 30, |       parseVariables, | ||||||
|       useCache: useCache ? true : false, |       parseFieldAndAssociations, | ||||||
|       refresh, |       parseBuilder, | ||||||
|     }); |       queryData, | ||||||
|  |       postProcess, | ||||||
|  |     ])(ctx, async () => {}); | ||||||
|   } catch (err) { |   } catch (err) { | ||||||
|     ctx.app.logger.error('charts query: ', err); |     ctx.app.logger.error('charts query: ', err); | ||||||
|     ctx.throw(500, err); |     ctx.throw(500, err); | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   await next(); |   await next(); | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										93
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										93
									
								
								yarn.lock
									
									
									
									
									
								
							| @ -4751,10 +4751,6 @@ | |||||||
|   version "1.7.1" |   version "1.7.1" | ||||||
|   resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498" |   resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498" | ||||||
| 
 | 
 | ||||||
| "@remix-run/router@1.7.2": |  | ||||||
|   version "1.7.2" |  | ||||||
|   resolved "https://registry.npmmirror.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8" |  | ||||||
| 
 |  | ||||||
| "@restart/hooks@^0.4.7": | "@restart/hooks@^0.4.7": | ||||||
|   version "0.4.9" |   version "0.4.9" | ||||||
|   resolved "https://registry.npmmirror.com/@restart/hooks/-/hooks-0.4.9.tgz#ad858fb39d99e252cccce19416adc18fc3f18fcb" |   resolved "https://registry.npmmirror.com/@restart/hooks/-/hooks-0.4.9.tgz#ad858fb39d99e252cccce19416adc18fc3f18fcb" | ||||||
| @ -6037,12 +6033,19 @@ | |||||||
|     "@types/prop-types" "*" |     "@types/prop-types" "*" | ||||||
|     "@types/react" "*" |     "@types/react" "*" | ||||||
| 
 | 
 | ||||||
| "@types/react-dom@17.x", "@types/react-dom@^17.0.0", "@types/react-dom@^18.0.0": | "@types/react-dom@17.x", "@types/react-dom@^17.0.0": | ||||||
|   version "17.0.20" |   version "17.0.20" | ||||||
|   resolved "https://registry.npmmirror.com/@types/react-dom/-/react-dom-17.0.20.tgz#e0c8901469d732b36d8473b40b679ad899da1b53" |   resolved "https://registry.npmmirror.com/@types/react-dom/-/react-dom-17.0.20.tgz#e0c8901469d732b36d8473b40b679ad899da1b53" | ||||||
|   dependencies: |   dependencies: | ||||||
|     "@types/react" "^17" |     "@types/react" "^17" | ||||||
| 
 | 
 | ||||||
|  | "@types/react-dom@^18.0.0": | ||||||
|  |   version "18.2.7" | ||||||
|  |   resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63" | ||||||
|  |   integrity sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/react" "*" | ||||||
|  | 
 | ||||||
| "@types/react-redux@^7.1.20": | "@types/react-redux@^7.1.20": | ||||||
|   version "7.1.25" |   version "7.1.25" | ||||||
|   resolved "https://registry.npmmirror.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88" |   resolved "https://registry.npmmirror.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88" | ||||||
| @ -6052,7 +6055,7 @@ | |||||||
|     hoist-non-react-statics "^3.3.0" |     hoist-non-react-statics "^3.3.0" | ||||||
|     redux "^4.0.0" |     redux "^4.0.0" | ||||||
| 
 | 
 | ||||||
| "@types/react@*", "@types/react@16 || 17 || 18", "@types/react@17.x", "@types/react@>=16.9.11", "@types/react@^17", "@types/react@^17.0.0", "@types/react@^18.0.0": | "@types/react@*", "@types/react@16 || 17 || 18", "@types/react@17.x", "@types/react@>=16.9.11", "@types/react@^17", "@types/react@^17.0.0": | ||||||
|   version "17.0.62" |   version "17.0.62" | ||||||
|   resolved "https://registry.npmmirror.com/@types/react/-/react-17.0.62.tgz#2efe8ddf8533500ec44b1334dd1a97caa2f860e3" |   resolved "https://registry.npmmirror.com/@types/react/-/react-17.0.62.tgz#2efe8ddf8533500ec44b1334dd1a97caa2f860e3" | ||||||
|   dependencies: |   dependencies: | ||||||
| @ -6060,6 +6063,15 @@ | |||||||
|     "@types/scheduler" "*" |     "@types/scheduler" "*" | ||||||
|     csstype "^3.0.2" |     csstype "^3.0.2" | ||||||
| 
 | 
 | ||||||
|  | "@types/react@^18.0.0": | ||||||
|  |   version "18.2.21" | ||||||
|  |   resolved "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9" | ||||||
|  |   integrity sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/prop-types" "*" | ||||||
|  |     "@types/scheduler" "*" | ||||||
|  |     csstype "^3.0.2" | ||||||
|  | 
 | ||||||
| "@types/readdir-glob@*": | "@types/readdir-glob@*": | ||||||
|   version "1.1.1" |   version "1.1.1" | ||||||
|   resolved "https://registry.npmmirror.com/@types/readdir-glob/-/readdir-glob-1.1.1.tgz#27ac2db283e6aa3d110b14ff9da44fcd1a5c38b1" |   resolved "https://registry.npmmirror.com/@types/readdir-glob/-/readdir-glob-1.1.1.tgz#27ac2db283e6aa3d110b14ff9da44fcd1a5c38b1" | ||||||
| @ -6263,7 +6275,17 @@ | |||||||
|     semver "^7.5.4" |     semver "^7.5.4" | ||||||
|     ts-api-utils "^1.0.1" |     ts-api-utils "^1.0.1" | ||||||
| 
 | 
 | ||||||
| "@typescript-eslint/parser@5.48.1", "@typescript-eslint/parser@^6.2.0": | "@typescript-eslint/parser@5.48.1": | ||||||
|  |   version "5.48.1" | ||||||
|  |   resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.48.1.tgz#d0125792dab7e232035434ab8ef0658154db2f10" | ||||||
|  |   integrity sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA== | ||||||
|  |   dependencies: | ||||||
|  |     "@typescript-eslint/scope-manager" "5.48.1" | ||||||
|  |     "@typescript-eslint/types" "5.48.1" | ||||||
|  |     "@typescript-eslint/typescript-estree" "5.48.1" | ||||||
|  |     debug "^4.3.4" | ||||||
|  | 
 | ||||||
|  | "@typescript-eslint/parser@^6.2.0": | ||||||
|   version "6.2.0" |   version "6.2.0" | ||||||
|   resolved "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-6.2.0.tgz#d37c30b0f459c6f39455335d8f4f085919a1c644" |   resolved "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-6.2.0.tgz#d37c30b0f459c6f39455335d8f4f085919a1c644" | ||||||
|   dependencies: |   dependencies: | ||||||
| @ -13010,7 +13032,7 @@ highlight.js@^10.1.0, highlight.js@^10.2.0: | |||||||
|   version "10.7.3" |   version "10.7.3" | ||||||
|   resolved "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" |   resolved "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" | ||||||
| 
 | 
 | ||||||
| history@5.3.0: | history@5.3.0, history@^5.2.0: | ||||||
|   version "5.3.0" |   version "5.3.0" | ||||||
|   resolved "https://registry.npmmirror.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" |   resolved "https://registry.npmmirror.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" | ||||||
|   dependencies: |   dependencies: | ||||||
| @ -15233,7 +15255,8 @@ koa-bodyparser@^4.3.0: | |||||||
| 
 | 
 | ||||||
| koa-compose@^4.1.0: | koa-compose@^4.1.0: | ||||||
|   version "4.1.0" |   version "4.1.0" | ||||||
|   resolved "https://registry.npmmirror.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" |   resolved "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" | ||||||
|  |   integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== | ||||||
| 
 | 
 | ||||||
| koa-convert@^2.0.0: | koa-convert@^2.0.0: | ||||||
|   version "2.0.0" |   version "2.0.0" | ||||||
| @ -19732,7 +19755,15 @@ react-copy-to-clipboard@^5.1.0: | |||||||
|     copy-to-clipboard "^3.3.1" |     copy-to-clipboard "^3.3.1" | ||||||
|     prop-types "^15.8.1" |     prop-types "^15.8.1" | ||||||
| 
 | 
 | ||||||
| react-dom@18.1.0, react-dom@18.x, react-dom@^18.0.0, react-dom@^18.2.0: | react-dom@18.1.0: | ||||||
|  |   version "18.1.0" | ||||||
|  |   resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f" | ||||||
|  |   integrity sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w== | ||||||
|  |   dependencies: | ||||||
|  |     loose-envify "^1.1.0" | ||||||
|  |     scheduler "^0.22.0" | ||||||
|  | 
 | ||||||
|  | react-dom@18.x, react-dom@^18.0.0, react-dom@^18.2.0: | ||||||
|   version "18.2.0" |   version "18.2.0" | ||||||
|   resolved "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" |   resolved "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" | ||||||
|   dependencies: |   dependencies: | ||||||
| @ -19895,18 +19926,34 @@ react-refresh@0.14.0, react-refresh@^0.14.0: | |||||||
|   version "0.14.0" |   version "0.14.0" | ||||||
|   resolved "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" |   resolved "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" | ||||||
| 
 | 
 | ||||||
| react-router-dom@6.3.0, react-router-dom@6.x, react-router-dom@^6.11.2: | react-router-dom@6.3.0: | ||||||
|  |   version "6.3.0" | ||||||
|  |   resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" | ||||||
|  |   integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== | ||||||
|  |   dependencies: | ||||||
|  |     history "^5.2.0" | ||||||
|  |     react-router "6.3.0" | ||||||
|  | 
 | ||||||
|  | react-router-dom@6.x, react-router-dom@^6.11.2: | ||||||
|   version "6.14.1" |   version "6.14.1" | ||||||
|   resolved "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.14.1.tgz#0ad7ba7abdf75baa61169d49f096f0494907a36f" |   resolved "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.14.1.tgz#0ad7ba7abdf75baa61169d49f096f0494907a36f" | ||||||
|   dependencies: |   dependencies: | ||||||
|     "@remix-run/router" "1.7.1" |     "@remix-run/router" "1.7.1" | ||||||
|     react-router "6.14.1" |     react-router "6.14.1" | ||||||
| 
 | 
 | ||||||
| react-router@6.14.1, react-router@6.3.0, react-router@^6.11.2: | react-router@6.14.1: | ||||||
|   version "6.14.2" |   version "6.14.1" | ||||||
|   resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.2.tgz#1f60994d8c369de7b8ba7a78d8f7ec23df76b300" |   resolved "https://registry.npmjs.org/react-router/-/react-router-6.14.1.tgz#5e82bcdabf21add859dc04b1859f91066b3a5810" | ||||||
|  |   integrity sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g== | ||||||
|   dependencies: |   dependencies: | ||||||
|     "@remix-run/router" "1.7.2" |     "@remix-run/router" "1.7.1" | ||||||
|  | 
 | ||||||
|  | react-router@6.3.0: | ||||||
|  |   version "6.3.0" | ||||||
|  |   resolved "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" | ||||||
|  |   integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== | ||||||
|  |   dependencies: | ||||||
|  |     history "^5.2.0" | ||||||
| 
 | 
 | ||||||
| react-side-effect@^2.1.0: | react-side-effect@^2.1.0: | ||||||
|   version "2.1.2" |   version "2.1.2" | ||||||
| @ -19922,7 +19969,14 @@ react-to-print@^2.14.7: | |||||||
|   version "2.14.13" |   version "2.14.13" | ||||||
|   resolved "https://registry.npmmirror.com/react-to-print/-/react-to-print-2.14.13.tgz#cd0349f7ef93c8af5120fac0ef6c4f3d100df490" |   resolved "https://registry.npmmirror.com/react-to-print/-/react-to-print-2.14.13.tgz#cd0349f7ef93c8af5120fac0ef6c4f3d100df490" | ||||||
| 
 | 
 | ||||||
| react@18.1.0, react@18.x, react@^18.0.0, react@^18.2.0: | react@18.1.0: | ||||||
|  |   version "18.1.0" | ||||||
|  |   resolved "https://registry.npmjs.org/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890" | ||||||
|  |   integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ== | ||||||
|  |   dependencies: | ||||||
|  |     loose-envify "^1.1.0" | ||||||
|  | 
 | ||||||
|  | react@18.x, react@^18.0.0, react@^18.2.0: | ||||||
|   version "18.2.0" |   version "18.2.0" | ||||||
|   resolved "https://registry.npmmirror.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" |   resolved "https://registry.npmmirror.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" | ||||||
|   dependencies: |   dependencies: | ||||||
| @ -20760,6 +20814,13 @@ saxes@^5.0.1: | |||||||
|   dependencies: |   dependencies: | ||||||
|     xmlchars "^2.2.0" |     xmlchars "^2.2.0" | ||||||
| 
 | 
 | ||||||
|  | scheduler@^0.22.0: | ||||||
|  |   version "0.22.0" | ||||||
|  |   resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8" | ||||||
|  |   integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ== | ||||||
|  |   dependencies: | ||||||
|  |     loose-envify "^1.1.0" | ||||||
|  | 
 | ||||||
| scheduler@^0.23.0: | scheduler@^0.23.0: | ||||||
|   version "0.23.0" |   version "0.23.0" | ||||||
|   resolved "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" |   resolved "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user