diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/index.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/index.ts index 7a8cd25e83..2ff3af19c2 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/index.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/index.ts @@ -11,3 +11,5 @@ export * from './useBaseVariable'; export * from './useDateVariable'; export * from './useUserVariable'; export * from './useVariableOptions'; +export * from './useURLSearchParamsVariable'; +export * from './useRoleVariable'; diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/filter/FilterVariableInput.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/filter/FilterVariableInput.tsx index 29ef9ef278..4e7793eb38 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/filter/FilterVariableInput.tsx +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/filter/FilterVariableInput.tsx @@ -12,30 +12,17 @@ import { VariableInput, VariableScopeProvider, getShouldChange, - useCurrentUserVariable, - useDatetimeVariable, CollectionProvider, } from '@nocobase/client'; import { useMemoizedFn } from 'ahooks'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect } from 'react'; +import { useGeneralVariableOptions } from '../hooks'; export const ChartFilterVariableInput: React.FC = (props) => { const { value, onChange, fieldSchema } = props; const collectionField = fieldSchema?.['x-collection-field'] || ''; const [collection] = collectionField.split('.'); - const { currentUserSettings } = useCurrentUserVariable({ - collectionField: { uiSchema: fieldSchema }, - uiSchema: fieldSchema, - }); - const { datetimeSettings } = useDatetimeVariable({ - operator: fieldSchema['x-component-props']?.['filter-operator'], - schema: fieldSchema, - noDisabled: true, - }); - const options = useMemo( - () => [currentUserSettings, datetimeSettings].filter(Boolean), - [datetimeSettings, currentUserSettings], - ); + const options = useGeneralVariableOptions(fieldSchema, fieldSchema['x-component-props']?.['filter-operator']); const schema = { ...fieldSchema, 'x-component': fieldSchema['x-component'] || 'Input', diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/filter.ts b/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/filter.ts index 6b3b794308..1b23d1d271 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/filter.ts +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/filter.ts @@ -17,6 +17,7 @@ import { useActionContext, useCollectionManager_deprecated, useDataSourceManager, + useVariables, } from '@nocobase/client'; import { useCallback, useContext, useMemo } from 'react'; import { ChartDataContext } from '../block/ChartDataProvider'; @@ -24,7 +25,7 @@ import { Schema } from '@formily/react'; import { useChartsTranslation } from '../locale'; import { ChartFilterContext } from '../filter/FilterProvider'; import { useMemoizedFn } from 'ahooks'; -import { parse } from '@nocobase/utils/client'; +import { flatten, parse, unflatten } from '@nocobase/utils/client'; import lodash from 'lodash'; import { getFormulaComponent, getValuesByPath } from '../utils'; import deepmerge from 'deepmerge'; @@ -103,6 +104,7 @@ export const useChartFilter = () => { const { fieldSchema } = useActionContext(); const action = fieldSchema?.['x-action']; const { fields: fieldProps, form } = useContext(ChartFilterContext); + const variables = useVariables(); const getChartFilterFields = ({ dataSource, @@ -405,6 +407,38 @@ export const useChartFilter = () => { .join(' / '); }); + const parseFilter = useCallback( + async (filterValue: any) => { + const flat = flatten(filterValue, { + breakOn({ key }) { + return key.startsWith('$') && key !== '$and' && key !== '$or'; + }, + transformValue(value) { + if (!(typeof value === 'string' && value.startsWith('{{$') && value?.endsWith('}}'))) { + return value; + } + if (['$user', '$date', '$nDate', '$nRole'].some((n) => value.includes(n))) { + return value; + } + const result = variables?.parseVariable(value); + return result; + }, + }); + await Promise.all( + Object.keys(flat).map(async (key) => { + flat[key] = await flat[key]; + if (flat[key] === undefined) { + delete flat[key]; + } + return flat[key]; + }), + ); + const result = unflatten(flat); + return result; + }, + [variables], + ); + return { filter, refresh, @@ -413,6 +447,7 @@ export const useChartFilter = () => { hasFilter, appendFilter, getTranslatedTitle, + parseFilter, }; }; diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/useVariableOptions.ts b/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/useVariableOptions.ts index a75a06ba68..957e1eb64c 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/useVariableOptions.ts +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/hooks/useVariableOptions.ts @@ -7,27 +7,49 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { useField } from '@formily/react'; -import { useCurrentUserVariable, useDatetimeVariable } from '@nocobase/client'; +import { ISchema, useField } from '@formily/react'; +import { + useCurrentRoleVariable, + useCurrentUserVariable, + useDatetimeVariable, + useURLSearchParamsVariable, +} from '@nocobase/client'; import { useMemo } from 'react'; import { useFilterVariable } from './filter'; -export const useVariableOptions = () => { - const field = useField(); - const { operator, schema } = field.data || {}; +export const useGeneralVariableOptions = ( + schema: ISchema, + operator?: { + value: string; + }, +) => { const { currentUserSettings } = useCurrentUserVariable({ collectionField: { uiSchema: schema }, uiSchema: schema, }); - const { datetimeSettings } = useDatetimeVariable({ operator, schema }); - const filterVariable = useFilterVariable(); + const { currentRoleSettings } = useCurrentRoleVariable({ uiSchema: schema }); + const { datetimeSettings } = useDatetimeVariable({ operator, schema, noDisabled: true }); + const { urlSearchParamsSettings } = useURLSearchParamsVariable(); const result = useMemo( - () => [currentUserSettings, datetimeSettings, filterVariable].filter(Boolean), - [datetimeSettings, currentUserSettings, filterVariable], + () => [currentUserSettings, currentRoleSettings, datetimeSettings, urlSearchParamsSettings].filter(Boolean), + [datetimeSettings, currentUserSettings, currentRoleSettings, urlSearchParamsSettings], ); if (!operator || !schema) return []; return result; }; + +export const useVariableOptions = () => { + const field = useField(); + const { operator, schema } = field.data || {}; + const filterVariable = useFilterVariable(); + const generalOptions = useGeneralVariableOptions(schema, operator); + + const result = useMemo(() => [...generalOptions, filterVariable].filter(Boolean), [generalOptions, filterVariable]); + + if (!operator || !schema) return []; + + return result; +}; diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererProvider.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererProvider.tsx index a4485c8091..8e6779859d 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererProvider.tsx +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererProvider.tsx @@ -14,6 +14,7 @@ import { MaybeCollectionProvider, useAPIClient, useDataSourceManager, + useParsedFilter, useRequest, } from '@nocobase/client'; import React, { createContext, useContext } from 'react'; @@ -82,59 +83,59 @@ export const ChartRendererProvider: React.FC = (props) => { const { query, config, collection, transform, dataSource = DEFAULT_DATA_SOURCE_KEY } = props; const { addChart } = useContext(ChartDataContext); const { ready, form, enabled } = useContext(ChartFilterContext); - const { getFilter, hasFilter, appendFilter } = useChartFilter(); + const { getFilter, hasFilter, appendFilter, parseFilter } = useChartFilter(); const schema = useFieldSchema(); const api = useAPIClient(); const service = useRequest( - (dataSource, collection, query, manual) => - new Promise((resolve, reject) => { - // Check if the chart is configured - if (!(collection && query?.measures?.length)) return resolve(undefined); - // If the filter block is enabled, the filter form is required to be rendered - if (enabled && !form) return resolve(undefined); - const filterValues = getFilter(); - const queryWithFilter = - !manual && hasFilter({ dataSource, collection, query }, filterValues) - ? appendFilter({ dataSource, collection, query }, filterValues) - : query; - api - .request({ - url: 'charts:query', - method: 'POST', - data: { - uid: schema?.['x-uid'], - dataSource, - collection, - ...queryWithFilter, - filter: removeUnparsableFilter(queryWithFilter.filter), - 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); - }) - .finally(() => { - if (!manual && schema?.['x-uid']) { - addChart(schema?.['x-uid'], { dataSource, collection, service, query }); - } - }) - .catch(reject); - }), + async (dataSource, collection, query, manual) => { + if (!(collection && query?.measures?.length)) return; + // Check if the chart is configured + // If the filter block is enabled, the filter form is required to be rendered + if (enabled && !form) return; + const filterValues = getFilter(); + const parsedFilter = await parseFilter(query.filter); + const parsedQuery = { ...query, filter: parsedFilter }; + const config = { dataSource, collection, query: parsedQuery }; + const queryWithFilter = + !manual && hasFilter(config, filterValues) ? appendFilter(config, filterValues) : parsedQuery; + try { + const res = await api.request({ + url: 'charts:query', + method: 'POST', + data: { + uid: schema?.['x-uid'], + dataSource, + collection, + ...queryWithFilter, + filter: removeUnparsableFilter(queryWithFilter.filter), + 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; + }), + }, + }); + return res?.data?.data; + } catch (error) { + console.error(error); + throw error; + } finally { + if (!manual && schema?.['x-uid']) { + addChart(schema?.['x-uid'], { dataSource, collection, service, query }); + } + } + }, { defaultParams: [dataSource, collection, query], // Wait until ChartFilterProvider is rendered and check the status of the filter form diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/server/actions/query.ts b/packages/plugins/@nocobase/plugin-data-visualization/src/server/actions/query.ts index d192b8dbb6..9917e7bbb8 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/server/actions/query.ts +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/server/actions/query.ts @@ -8,11 +8,11 @@ */ import { Context, Next } from '@nocobase/actions'; -import { Field, FilterParser, snakeCase } from '@nocobase/database'; +import { Field, FilterParser } from '@nocobase/database'; import { formatter } from './formatter'; import compose from 'koa-compose'; -import { parseFilter, getDateVars } from '@nocobase/utils'; import { Cache } from '@nocobase/cache'; +import { middlewares } from '@nocobase/server'; type MeasureProps = { field: string | string[]; @@ -259,52 +259,11 @@ export const parseFieldAndAssociations = async (ctx: Context, next: Next) => { export const parseVariables = async (ctx: Context, next: Next) => { const { filter } = ctx.action.params.values; - if (!filter) { - return next(); - } - const isNumeric = (str: any) => { - if (typeof str === 'number') return true; - if (typeof str != 'string') return false; - return !isNaN(str as any) && !isNaN(parseFloat(str)); - }; - const getUser = () => { - return async ({ fields }) => { - const userFields = fields.filter((f) => f && ctx.db.getFieldByPath('users.' + f)); - ctx.logger?.info('parse filter variables', { userFields, method: 'parseVariables' }); - if (!ctx.state.currentUser) { - return; - } - if (!userFields.length) { - return; - } - const user = await ctx.db.getRepository('users').findOne({ - filterByTk: ctx.state.currentUser.id, - fields: userFields, - }); - ctx.logger?.info('parse filter variables', { - $user: user?.toJSON(), - method: 'parseVariables', - }); - 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(), - }, + ctx.action.params.filter = filter; + await middlewares.parseVariables(ctx, async () => { + ctx.action.params.values.filter = ctx.action.params.filter; + await next(); }); - await next(); }; export const cacheMiddleware = async (ctx: Context, next: Next) => {