feat(data-vi): support for using url params and current role variables (#4586)

* feat(data-vi): support for using url params and current role variable

* fix: bug
This commit is contained in:
YANG QIA 2024-06-07 11:43:11 +08:00 committed by GitHub
parent 837f4f4158
commit 34108f1fcb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 129 additions and 123 deletions

View File

@ -11,3 +11,5 @@ export * from './useBaseVariable';
export * from './useDateVariable';
export * from './useUserVariable';
export * from './useVariableOptions';
export * from './useURLSearchParamsVariable';
export * from './useRoleVariable';

View File

@ -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<any> = (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',

View File

@ -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,
};
};

View File

@ -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<any>();
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<any>();
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;
};

View File

@ -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<ChartRendererProps> = (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

View File

@ -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) => {