mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-02 20:27:49 +08:00
feat(client): add new variable named 'URL search params' and support link action (#4506)
* feat: support link action * feat(client): add new variable named 'URL search params' * chore: add translation * fix: avoid crashing * chore: fix failed test * feat: link action * feat: link action * fix: remove filter parameters with undefined values * feat: link action * feat: add support for default values in filter form fields * refactor: code improve * refactor: locale improve * refactor: locale improve * test: add e2e test * refactor: locale improve * refactor: locale improve * fix: resolve operation issues with variables * refactor: code improve * chore: enable direct selection of variables as default value * chore: use qs to parse query string * fix: menu selectKeys (T-4373) * refactor: use qs to stringify search params * refactor: locale improve * refactor: locale improve * chore: fix failed tests * fix: resolve issue where setting Data scope is not work * chore: fix failed e2e tests * chore: make e2e tests more stable * chore: add translation * chore: make e2e tests more stable * fix: resolve the issue of error when saving data scope * feat: trigger variable parsing after context change * test: add unit tests * test: add e2e test * refactor: extract template * chore: fix failed unit tests * chore: fix failed e2e test * fix(Link): hide linkage rules in top link (T-4410) * fix(permission): remove URL search params variable from data scope * chore: make more stable * chore: make e2e test more stable * fix(Link): reduce size for variable * fix: clear previous context (T-4449) * fix(calendar, map): resolve initial data scope setting error (T-4450) * fix: correct concatenation of query string (T-4453) --------- Co-authored-by: katherinehhh <katherine_15995@163.com> Co-authored-by: jack zhang <1098626505@qq.com>
This commit is contained in:
parent
0b8f762d8b
commit
f66edb5d27
@ -77,22 +77,26 @@ const InternalDetailsBlockProvider = (props) => {
|
||||
const useCompatDetailsBlockParams = (props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
||||
let params;
|
||||
let params,
|
||||
parseVariableLoading = false;
|
||||
// 1. 新版本的 schema 存在 x-use-decorator-props 属性
|
||||
if (fieldSchema['x-use-decorator-props']) {
|
||||
params = props?.params;
|
||||
parseVariableLoading = props?.parseVariableLoading;
|
||||
} else {
|
||||
// 2. 旧版本的 schema 不存在 x-use-decorator-props 属性
|
||||
// 因为 schema 中是否存在 x-use-decorator-props 是固定不变的,所以这里可以使用 hooks
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
params = useDetailsWithPaginationBlockParams(props);
|
||||
const parsedParams = useDetailsWithPaginationBlockParams(props);
|
||||
params = parsedParams.params;
|
||||
parseVariableLoading = parsedParams.parseVariableLoading;
|
||||
}
|
||||
|
||||
return params;
|
||||
return { params, parseVariableLoading };
|
||||
};
|
||||
|
||||
export const DetailsBlockProvider = withDynamicSchemaProps((props) => {
|
||||
const params = useCompatDetailsBlockParams(props);
|
||||
const { params, parseVariableLoading } = useCompatDetailsBlockParams(props);
|
||||
const record = useCollectionRecordData();
|
||||
const { association, dataSource } = props;
|
||||
const { getCollection } = useCollectionManager_deprecated(dataSource);
|
||||
@ -104,7 +108,7 @@ export const DetailsBlockProvider = withDynamicSchemaProps((props) => {
|
||||
detailFlag = __collection === collection;
|
||||
}
|
||||
|
||||
if (!detailFlag) {
|
||||
if (!detailFlag || parseVariableLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -67,8 +67,8 @@ const InternalTableBlockProvider = (props: Props) => {
|
||||
}, [service?.loading, fieldSchema]);
|
||||
|
||||
const setExpandFlagValue = useCallback(
|
||||
(falg) => {
|
||||
setExpandFlag(falg || !expandFlag);
|
||||
(flag) => {
|
||||
setExpandFlag(flag || !expandFlag);
|
||||
},
|
||||
[expandFlag],
|
||||
);
|
||||
@ -106,18 +106,22 @@ const InternalTableBlockProvider = (props: Props) => {
|
||||
const useTableBlockParamsCompat = (props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
||||
let params;
|
||||
let params,
|
||||
parseVariableLoading = false;
|
||||
// 1. 新版本的 schema 存在 x-use-decorator-props 属性
|
||||
if (fieldSchema['x-use-decorator-props']) {
|
||||
params = props.params;
|
||||
parseVariableLoading = props.parseVariableLoading;
|
||||
} else {
|
||||
// 2. 旧版本的 schema 不存在 x-use-decorator-props 属性
|
||||
// 因为 schema 中是否存在 x-use-decorator-props 是固定不变的,所以这里可以使用 hooks
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
params = useTableBlockParams(props);
|
||||
const tableBlockParams = useTableBlockParams(props);
|
||||
params = tableBlockParams.params;
|
||||
parseVariableLoading = tableBlockParams.parseVariableLoading;
|
||||
}
|
||||
|
||||
return params;
|
||||
return { params, parseVariableLoading };
|
||||
};
|
||||
|
||||
export const TableBlockProvider = withDynamicSchemaProps((props) => {
|
||||
@ -127,7 +131,7 @@ export const TableBlockProvider = withDynamicSchemaProps((props) => {
|
||||
const { getCollection, getCollectionField } = useCollectionManager_deprecated(props.dataSource);
|
||||
const collection = getCollection(props.collection, props.dataSource);
|
||||
const { treeTable } = fieldSchema?.['x-decorator-props'] || {};
|
||||
const params = useTableBlockParamsCompat(props);
|
||||
const { params, parseVariableLoading } = useTableBlockParamsCompat(props);
|
||||
|
||||
let childrenColumnName = 'children';
|
||||
|
||||
@ -148,6 +152,11 @@ export const TableBlockProvider = withDynamicSchemaProps((props) => {
|
||||
}
|
||||
const form = useMemo(() => createForm(), [treeTable]);
|
||||
|
||||
// 在解析变量的时候不渲染,避免因为重复请求数据导致的资源浪费
|
||||
if (parseVariableLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SchemaComponentOptions scope={{ treeTable }}>
|
||||
<FormContext.Provider value={form}>
|
||||
|
@ -257,11 +257,11 @@ export const TableSelectorProvider = withDynamicSchemaProps((props: TableSelecto
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
const { filter: parsedFilter } = useParsedFilter({
|
||||
const { filter: parsedFilter, parseVariableLoading } = useParsedFilter({
|
||||
filterOption: params?.filter,
|
||||
});
|
||||
|
||||
if (!_.isEmpty(params?.filter) && _.isEmpty(parsedFilter)) {
|
||||
if ((!_.isEmpty(params?.filter) && _.isEmpty(parsedFilter)) || parseVariableLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,152 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { appendQueryStringToUrl, parseVariablesAndChangeParamsToQueryString, reduceValueSize } from '../../hooks/index';
|
||||
|
||||
describe('parseVariablesAndChangeParamsToQueryString', () => {
|
||||
it('should parse variables and change params to query string', async () => {
|
||||
const searchParams = [
|
||||
{ name: 'param1', value: '{{ $var1.value }}' },
|
||||
{ name: 'param2', value: 'value2' },
|
||||
{ name: 'param3', value: 'value3' },
|
||||
];
|
||||
const variables: any = {
|
||||
parseVariable: vi.fn().mockResolvedValue('parsedValue'),
|
||||
};
|
||||
const localVariables: any = [
|
||||
{ name: '$var1', ctx: { value: 'localValue1' } },
|
||||
{ name: '$var2', ctx: { value: 'localValue2' } },
|
||||
];
|
||||
const replaceVariableValue = vi.fn().mockResolvedValue('replacedValue');
|
||||
|
||||
const result = await parseVariablesAndChangeParamsToQueryString({
|
||||
searchParams,
|
||||
variables,
|
||||
localVariables,
|
||||
replaceVariableValue,
|
||||
});
|
||||
|
||||
expect(variables.parseVariable).toHaveBeenCalledTimes(1);
|
||||
expect(variables.parseVariable).toHaveBeenCalledWith('{{ $var1.value }}', localVariables);
|
||||
expect(replaceVariableValue).toHaveBeenCalledTimes(2);
|
||||
expect(replaceVariableValue).toHaveBeenCalledWith('value2', variables, localVariables);
|
||||
expect(replaceVariableValue).toHaveBeenCalledWith('value3', variables, localVariables);
|
||||
|
||||
expect(result).toBe('param1=parsedValue¶m2=replacedValue¶m3=replacedValue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reduceValueSize', () => {
|
||||
it('should reduce the size of the value', () => {
|
||||
const value = {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
key3: 'value3',
|
||||
};
|
||||
|
||||
const result = reduceValueSize(value);
|
||||
|
||||
expect(result).toEqual({
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
key3: 'value3',
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove keys with string values longer than 100 characters', () => {
|
||||
const value = {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
key3: 'value3'.repeat(20),
|
||||
};
|
||||
|
||||
const result = reduceValueSize(value);
|
||||
|
||||
expect(result).toEqual({
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reduce the size of nested objects', () => {
|
||||
const value = {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
key3: {
|
||||
nestedKey1: 'nestedValue1',
|
||||
nestedKey2: 'nestedValue2',
|
||||
nestedKey3: 'nestedValue3',
|
||||
},
|
||||
};
|
||||
|
||||
const result = reduceValueSize(value);
|
||||
|
||||
expect(result).toEqual({
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reduce the size of nested arrays', () => {
|
||||
const value = {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
key3: ['value1', 'value2', 'value3'],
|
||||
};
|
||||
|
||||
const result = reduceValueSize(value);
|
||||
|
||||
expect(result).toEqual({
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reduce the size of arrays', () => {
|
||||
const value = ['value1', 'value2', 'value3'.repeat(20)];
|
||||
const result = reduceValueSize(value);
|
||||
expect(result).toEqual(['value1', 'value2', 'value3'.repeat(20)]);
|
||||
|
||||
const value2 = [
|
||||
'value1',
|
||||
'value2',
|
||||
{
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
key3: {
|
||||
nestedKey1: 'nestedValue1',
|
||||
nestedKey2: 'nestedValue2',
|
||||
nestedKey3: 'nestedValue3',
|
||||
},
|
||||
},
|
||||
];
|
||||
const result2 = reduceValueSize(value2);
|
||||
expect(result2).toEqual(['value1', 'value2', { key1: 'value1', key2: 'value2' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('appendQueryStringToUrl', () => {
|
||||
it('should append query string to the URL', () => {
|
||||
const url = 'https://example.com';
|
||||
const queryString = 'param1=value1¶m2=value2';
|
||||
|
||||
const result = appendQueryStringToUrl(url, queryString);
|
||||
|
||||
expect(result).toBe('https://example.com?param1=value1¶m2=value2');
|
||||
});
|
||||
|
||||
it('should append query string to the URL with existing query parameters', () => {
|
||||
const url = 'https://example.com?existingParam=value';
|
||||
const queryString = 'param1=value1¶m2=value2';
|
||||
|
||||
const result = appendQueryStringToUrl(url, queryString);
|
||||
|
||||
expect(result).toBe('https://example.com?existingParam=value¶m1=value1¶m2=value2');
|
||||
});
|
||||
});
|
@ -10,11 +10,13 @@
|
||||
import { Field, Form } from '@formily/core';
|
||||
import { SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { untracked } from '@formily/reactive';
|
||||
import { evaluators } from '@nocobase/evaluators/client';
|
||||
import { isURL, parse } from '@nocobase/utils/client';
|
||||
import { App, message } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import get from 'lodash/get';
|
||||
import omit from 'lodash/omit';
|
||||
import qs from 'qs';
|
||||
import { ChangeEvent, useCallback, useContext, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@ -35,8 +37,10 @@ import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/Tree
|
||||
import { useRecord } from '../../record-provider';
|
||||
import { removeNullCondition, useActionContext, useCompile } from '../../schema-component';
|
||||
import { isSubMode } from '../../schema-component/antd/association-field/util';
|
||||
import { replaceVariables } from '../../schema-component/antd/form-v2/utils';
|
||||
import { useCurrentUserContext } from '../../user';
|
||||
import { useLocalVariables, useVariables } from '../../variables';
|
||||
import { VariableOption, VariablesContextType } from '../../variables/types';
|
||||
import { isVariable } from '../../variables/utils/isVariable';
|
||||
import { transformVariableValue } from '../../variables/utils/transformVariableValue';
|
||||
import { useBlockRequestContext, useFilterByTk, useParamsFromRecord } from '../BlockProvider';
|
||||
@ -1446,3 +1450,131 @@ async function resetFormCorrectly(form: Form) {
|
||||
});
|
||||
await form.reset();
|
||||
}
|
||||
|
||||
export function appendQueryStringToUrl(url: string, queryString: string) {
|
||||
return url + (url.includes('?') ? '&' : '?') + queryString;
|
||||
}
|
||||
|
||||
export function useLinkActionProps() {
|
||||
const navigate = useNavigate();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const url = fieldSchema?.['x-component-props']?.['url'];
|
||||
const searchParams = fieldSchema?.['x-component-props']?.['params'] || [];
|
||||
const variables = useVariables();
|
||||
const localVariables = useLocalVariables();
|
||||
|
||||
return {
|
||||
type: 'default',
|
||||
async onClick() {
|
||||
if (!url) {
|
||||
message.warning(t('Please configure the URL'));
|
||||
return;
|
||||
}
|
||||
const queryString = await parseVariablesAndChangeParamsToQueryString({
|
||||
searchParams,
|
||||
variables,
|
||||
localVariables,
|
||||
replaceVariableValue,
|
||||
});
|
||||
const link = appendQueryStringToUrl(url, queryString);
|
||||
if (link) {
|
||||
if (isURL(link)) {
|
||||
window.open(link, '_blank');
|
||||
} else {
|
||||
navigate(link);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function replaceVariableValue(url: string, variables: VariablesContextType, localVariables: VariableOption[]) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
const { evaluate } = evaluators.get('string');
|
||||
// 解析如 `{{$user.name}}` 之类的变量
|
||||
const { exp, scope: expScope } = await replaceVariables(url, {
|
||||
variables,
|
||||
localVariables,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = evaluate(exp, { now: () => new Date().toString(), ...expScope });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseVariablesAndChangeParamsToQueryString({
|
||||
searchParams,
|
||||
variables,
|
||||
localVariables,
|
||||
replaceVariableValue,
|
||||
}: {
|
||||
searchParams: { name: string; value: any }[];
|
||||
variables: VariablesContextType;
|
||||
localVariables: VariableOption[];
|
||||
replaceVariableValue: (
|
||||
url: string,
|
||||
variables: VariablesContextType,
|
||||
localVariables: VariableOption[],
|
||||
) => Promise<any>;
|
||||
}) {
|
||||
const parsed = await Promise.all(
|
||||
searchParams.map(async ({ name, value }) => {
|
||||
if (typeof value === 'string') {
|
||||
if (isVariable(value)) {
|
||||
const result = await variables.parseVariable(value, localVariables);
|
||||
return { name, value: result };
|
||||
}
|
||||
const result = await replaceVariableValue(value, variables, localVariables);
|
||||
return { name, value: result };
|
||||
}
|
||||
return { name, value };
|
||||
}),
|
||||
);
|
||||
|
||||
const params = {};
|
||||
|
||||
for (const { name, value } of parsed) {
|
||||
if (name && value) {
|
||||
params[name] = reduceValueSize(value);
|
||||
}
|
||||
}
|
||||
|
||||
return qs.stringify(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 去除 value 是一个对象或者数组的 key
|
||||
* 2. 去除 value 是一个字符串长度超过 100 个字符的 key
|
||||
*/
|
||||
export function reduceValueSize(value: any) {
|
||||
if (_.isPlainObject(value)) {
|
||||
const result = {};
|
||||
Object.keys(value).forEach((key) => {
|
||||
if (_.isPlainObject(value[key]) || _.isArray(value[key])) {
|
||||
return;
|
||||
}
|
||||
if (_.isString(value[key]) && value[key].length > 100) {
|
||||
return;
|
||||
}
|
||||
result[key] = value[key];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
if (_.isArray(value)) {
|
||||
return value.map((item) => {
|
||||
if (_.isPlainObject(item) || _.isArray(item)) {
|
||||
return reduceValueSize(item);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
@ -25,12 +25,15 @@ import { isVariable } from '../../variables/utils/isVariable';
|
||||
export function useParsedFilter({ filterOption }: { filterOption: any }) {
|
||||
const { parseFilter, findVariable } = useParseDataScopeFilter();
|
||||
const [filter, setFilter] = useState({});
|
||||
const [parseVariableLoading, setParseVariableLoading] = useState(!!filterOption);
|
||||
|
||||
useEffect(() => {
|
||||
if (!filterOption) return;
|
||||
|
||||
const _run = async () => {
|
||||
setParseVariableLoading(true);
|
||||
const result = await parseFilter(filterOption);
|
||||
setParseVariableLoading(false);
|
||||
setFilter(result);
|
||||
};
|
||||
_run();
|
||||
@ -64,7 +67,12 @@ export function useParsedFilter({ filterOption }: { filterOption: any }) {
|
||||
});
|
||||
return flat;
|
||||
}, run);
|
||||
}, [JSON.stringify(filterOption)]);
|
||||
}, [JSON.stringify(filterOption), parseVariableLoading]);
|
||||
|
||||
return { filter };
|
||||
return {
|
||||
/** 数据范围的筛选参数 */
|
||||
filter,
|
||||
/** 表示是否正在解析筛选参数中的变量 */
|
||||
parseVariableLoading,
|
||||
};
|
||||
}
|
||||
|
@ -826,5 +826,7 @@
|
||||
"Plugin settings": "Plugin settings",
|
||||
"Menu": "Menu",
|
||||
"Drag and drop sorting field": "Drag and drop sorting field",
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "This variable has been deprecated and can be replaced with \"Current form\""
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "This variable has been deprecated and can be replaced with \"Current form\"",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.",
|
||||
"URL search params": "URL search params"
|
||||
}
|
||||
|
@ -758,5 +758,7 @@
|
||||
"Home page": "Página de inicio",
|
||||
"Handbook": "Manual de usuario",
|
||||
"License": "Licencia",
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "La variable ha sido obsoleta; \"Formulario actual\" puede ser utilizada como sustituto"
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "La variable ha sido obsoleta; \"Formulario actual\" puede ser utilizada como sustituto",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "El valor de esta variable se deriva de la cadena de consulta de la URL de la página. Esta variable sólo puede utilizarse normalmente cuando la página tiene una cadena de consulta.",
|
||||
"URL search params": "Parámetros de búsqueda de URL"
|
||||
}
|
||||
|
@ -778,5 +778,7 @@
|
||||
"Home page": "Page d'accueil",
|
||||
"Handbook": "Manuel de l'utilisateur",
|
||||
"License": "Licence",
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "La variable a été obsolète ; \"Formulaire actuel\" peut être utilisé comme substitut"
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "La variable a été obsolète ; \"Formulaire actuel\" peut être utilisé comme substitut",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "La valeur de cette variable est dérivée de la chaîne de requête de l'URL de la page. Cette variable ne peut être utilisée normalement que lorsque la page a une chaîne de requête.",
|
||||
"URL search params": "Paramètres de recherche d'URL"
|
||||
}
|
||||
|
@ -697,5 +697,7 @@
|
||||
"Home page": "ホームページ",
|
||||
"Handbook": "ユーザーマニュアル",
|
||||
"License": "ライセンス",
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "この変数は非推奨です。代わりに「現在のフォーム」を使用してください"
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "この変数は非推奨です。代わりに「現在のフォーム」を使用してください",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "この変数の値はページURLのクエリ文字列から取得されます。この変数は、ページにクエリ文字列がある場合にのみ正常に使用できます。",
|
||||
"URL search params": "URL検索パラメータ"
|
||||
}
|
||||
|
@ -869,5 +869,7 @@
|
||||
"Home page": "홈페이지",
|
||||
"Handbook": "사용자 매뉴얼",
|
||||
"License": "라이선스",
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "변수가 폐기되었습니다. \"현재 폼\"을 대체로 사용할 수 있습니다"
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "변수가 폐기되었습니다. \"현재 폼\"을 대체로 사용할 수 있습니다",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "이 변수의 값은 페이지 URL의 쿼리 문자열에서 파생됩니다. 이 변수는 페이지에 쿼리 문자열이 있는 경우에만 정상적으로 사용할 수 있습니다.",
|
||||
"URL search params": "URL 검색 매개변수"
|
||||
}
|
||||
|
@ -734,5 +734,7 @@
|
||||
"Home page": "Página inicial",
|
||||
"Handbook": "Manual do usuário",
|
||||
"License": "Licença",
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "A variável foi descontinuada; \"Formulário atual\" pode ser usada como substituto"
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "A variável foi descontinuada; \"Formulário atual\" pode ser usada como substituto",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "O valor desta variável é derivado da string de consulta da URL da página. Esta variável só pode ser usada normalmente quando a página tem uma string de consulta.",
|
||||
"URL search params": "Parâmetros de pesquisa de URL"
|
||||
}
|
||||
|
@ -572,5 +572,7 @@
|
||||
"Home page": "Домашняя страница",
|
||||
"Handbook": "Руководство пользователя",
|
||||
"License": "Лицензия",
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "Переменная устарела; \"Текущая форма\" может быть использована в качестве замены"
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "Переменная устарела; \"Текущая форма\" может быть использована в качестве замены",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "Значение этой переменной происходит из строки запроса URL страницы. Эта переменная может использоваться только в том случае, если у страницы есть строка запроса.",
|
||||
"URL search params": "Параметры поиска URL"
|
||||
}
|
||||
|
@ -570,5 +570,7 @@
|
||||
"Home page": "Anasayfa",
|
||||
"Handbook": "Kullanıcı kılavuzu",
|
||||
"License": "Lisans",
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "Değişken kullanımdan kaldırıldı; \"Geçerli form\" yerine kullanılabilir"
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "Değişken kullanımdan kaldırıldı; \"Geçerli form\" yerine kullanılabilir",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "Bu değişkenin değeri sayfa URL'sinin sorgu dizgisinden türetilir. Bu değişken, sayfanın bir sorgu dizgisi olduğunda yalnızca normal olarak kullanılabilir.",
|
||||
"URL search params": "URL arama parametreleri"
|
||||
}
|
||||
|
@ -778,5 +778,7 @@
|
||||
"Home page": "Домашня сторінка",
|
||||
"Handbook": "Посібник користувача",
|
||||
"License": "Ліцензія",
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "Змінна була застарілою; \"Поточна форма\" може бути використана як заміна"
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "Змінна була застарілою; \"Поточна форма\" може бути використана як заміна",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "Значення цієї змінної походить з рядка запиту URL-адреси сторінки. Цю змінну можна використовувати нормально лише тоді, коли у сторінки є рядок запиту.",
|
||||
"URL search params": "Параметри пошуку URL"
|
||||
}
|
||||
|
@ -949,5 +949,13 @@
|
||||
"Fixed": "固定列",
|
||||
"Set block height": "设置区块高度",
|
||||
"Specify height": "指定高度",
|
||||
"Full height": "全高"
|
||||
"Full height": "全高",
|
||||
"Please configure the URL": "请配置URL",
|
||||
"URL": "URL",
|
||||
"Search parameters": "URL 查询参数",
|
||||
"Do not concatenate search params in the URL": "查询参数不要在 URL 里拼接",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "该变量的值是根据页面 URL 的 query string 得来的,只有当页面存在 query string 的时候,该变量才能正常使用。",
|
||||
"Edit link":"编辑链接",
|
||||
"Add parameter":"添加参数",
|
||||
"URL search params": "URL 查询参数"
|
||||
}
|
||||
|
@ -867,5 +867,7 @@
|
||||
"Home page": "主頁",
|
||||
"Handbook": "使用手冊",
|
||||
"License": "許可證",
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "該變數已被棄用,可以使用“當前表單”作為替代"
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "該變數已被棄用,可以使用“當前表單”作為替代",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "該變數的值來自頁面 URL 的查詢字符串,只有當頁面有查詢字符串時,該變數才能正常使用。",
|
||||
"URL search params": "URL 查詢參數"
|
||||
}
|
||||
|
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { expect, test } from '@nocobase/test/e2e';
|
||||
import { oneEmptyTableWithUsers } from './templates';
|
||||
|
||||
test.describe('Link', () => {
|
||||
test('basic', async ({ page, mockPage, mockRecords }) => {
|
||||
const nocoPage = await mockPage(oneEmptyTableWithUsers).waitForInit();
|
||||
const users = await mockRecords('users', 2, 0);
|
||||
await nocoPage.goto();
|
||||
|
||||
// 1. create a new Link button
|
||||
await page.getByRole('button', { name: 'Actions', exact: true }).hover();
|
||||
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Link' }).click();
|
||||
|
||||
// 2. config the Link button
|
||||
await page.getByLabel('action-Action.Link-Link-customize:link-users-table-0').hover();
|
||||
await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:link-users' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Edit link' }).click();
|
||||
await page
|
||||
.getByLabel('block-item-Variable.TextArea-users-table-URL')
|
||||
.getByLabel('textbox')
|
||||
.fill(await nocoPage.getUrl());
|
||||
await page.getByPlaceholder('Name').fill('id');
|
||||
await page.getByLabel('block-item-ArrayItems-users-').getByLabel('variable-button').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'plus Add parameter' }).click();
|
||||
await page.getByPlaceholder('Name').nth(1).fill('name');
|
||||
await page.getByLabel('variable-button').nth(2).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Username' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
|
||||
// 3. click the Link button then config data scope for the Table block using "URL search params" variable
|
||||
await page.getByLabel('action-Action.Link-Link-customize:link-users-table-0').click();
|
||||
await page.getByLabel('block-item-CardItem-users-').hover();
|
||||
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Set the data scope' }).click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByTestId('select-filter-field').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||
await page.getByTestId('select-filter-operator').click();
|
||||
await page.getByRole('option', { name: 'is not' }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'URL search params right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'id', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: 'nocobase', exact: true })).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: users[0].username, exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: users[1].username, exact: true })).toBeVisible();
|
||||
|
||||
// 4. click the Link button,check the data of the table block
|
||||
await page.getByLabel('action-Action.Link-Link-customize:link-users-table-0').click();
|
||||
await expect(page.getByRole('button', { name: users[0].username, exact: true })).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'nocobase', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: users[1].username, exact: true })).toBeVisible();
|
||||
});
|
||||
});
|
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export const oneEmptyTableWithUsers = {
|
||||
pageSchema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
properties: {
|
||||
hoy3gninc4d: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'page:addBlock',
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
properties: {
|
||||
'6q8k2r2zg8b': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
properties: {
|
||||
'5pelgrmw1uv': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
properties: {
|
||||
jinafp4khmd: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-acl-action': 'users:list',
|
||||
'x-use-decorator-props': 'useTableBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
dataSource: 'main',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
},
|
||||
rowKey: 'id',
|
||||
showIndex: true,
|
||||
dragSort: false,
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:table',
|
||||
'x-component': 'CardItem',
|
||||
'x-filter-targets': [],
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-initializer': 'table:configureActions',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
'x-uid': '57i5fodavmy',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
'1c78hfblr62': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-initializer': 'table:configureColumns',
|
||||
'x-component': 'TableV2',
|
||||
'x-use-component-props': 'useTableBlockProps',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-designer': 'TableV2.ActionColumnDesigner',
|
||||
'x-initializer': 'table:configureItemActions',
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
properties: {
|
||||
sq7wrsmu8k6: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
'x-uid': '9wjvgdr7qlp',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'c2kd07xlalt',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
kce2z62jj3e: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-toolbar': 'TableColumnSchemaToolbar',
|
||||
'x-settings': 'fieldSettings:TableColumn',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
properties: {
|
||||
id: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
'x-collection-field': 'users.id',
|
||||
'x-component': 'CollectionField',
|
||||
'x-component-props': {},
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': null,
|
||||
'x-decorator-props': {
|
||||
labelStyle: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
'x-uid': '5dwr9au92jy',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'd3o6bo2bntr',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
cxivkl2pz2e: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-toolbar': 'TableColumnSchemaToolbar',
|
||||
'x-settings': 'fieldSettings:TableColumn',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
properties: {
|
||||
username: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
'x-collection-field': 'users.username',
|
||||
'x-component': 'CollectionField',
|
||||
'x-component-props': {
|
||||
ellipsis: true,
|
||||
},
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': null,
|
||||
'x-decorator-props': {
|
||||
labelStyle: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.0.0-alpha.17',
|
||||
'x-uid': '6zd6cdztudd',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'bstw44sol77',
|
||||
'x-async': false,
|
||||
'x-index': 3,
|
||||
},
|
||||
},
|
||||
'x-uid': 'tesn3o6xlvo',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'am04rxkwyrn',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'l6m3f6u2opr',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'tgnms5xwgtj',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'cgq9lp3869e',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'py05yxxke4g',
|
||||
'x-async': true,
|
||||
'x-index': 1,
|
||||
},
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useSchemaInitializerItem } from '../../../application';
|
||||
import { BlockInitializer } from '../../../schema-initializer/items';
|
||||
|
||||
export const LinkActionInitializer = (props) => {
|
||||
const schema = {
|
||||
type: 'void',
|
||||
title: '{{ t("Link") }}',
|
||||
'x-action': 'customize:link',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'actionSettings:link',
|
||||
'x-component': props?.['x-component'] || 'Action.Link',
|
||||
'x-use-component-props': 'useLinkActionProps',
|
||||
};
|
||||
|
||||
const itemConfig = useSchemaInitializerItem();
|
||||
return <BlockInitializer {...itemConfig} schema={schema} item={itemConfig} />;
|
||||
};
|
@ -0,0 +1,181 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
import { css } from '@emotion/css';
|
||||
import { ArrayItems } from '@formily/antd-v5';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollectionRecord, useDesignable, useFormBlockContext, useRecord } from '../../../';
|
||||
import { useSchemaToolbar } from '../../../application';
|
||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||
import { useCollection_deprecated } from '../../../collection-manager';
|
||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||
import { SchemaSettingsLinkageRules, SchemaSettingsModalItem } from '../../../schema-settings';
|
||||
import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks/useVariableOptions';
|
||||
|
||||
export function SchemaSettingsActionLinkItem() {
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const { form } = useFormBlockContext();
|
||||
const record = useRecord();
|
||||
const scope = useVariableOptions({
|
||||
collectionField: { uiSchema: fieldSchema },
|
||||
form,
|
||||
record,
|
||||
uiSchema: fieldSchema,
|
||||
noDisabled: true,
|
||||
});
|
||||
return (
|
||||
<SchemaSettingsModalItem
|
||||
title={t('Edit link')}
|
||||
components={{ ArrayItems }}
|
||||
schema={{
|
||||
type: 'object',
|
||||
title: t('Edit link'),
|
||||
properties: {
|
||||
url: {
|
||||
title: t('URL'),
|
||||
type: 'string',
|
||||
default: fieldSchema?.['x-component-props']?.['url'],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.TextArea',
|
||||
'x-component-props': {
|
||||
scope,
|
||||
changeOnSelect: true,
|
||||
},
|
||||
description: t('Do not concatenate search params in the URL'),
|
||||
},
|
||||
params: {
|
||||
type: 'array',
|
||||
'x-component': 'ArrayItems',
|
||||
'x-decorator': 'FormItem',
|
||||
title: `{{t("Search parameters")}}`,
|
||||
default: fieldSchema?.['x-component-props']?.params || [{}],
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
space: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
flexWrap: 'nowrap',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
className: css`
|
||||
& > .ant-space-item:first-child,
|
||||
& > .ant-space-item:last-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`,
|
||||
},
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: `{{t("Name")}}`,
|
||||
},
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.TextArea',
|
||||
'x-component-props': {
|
||||
scope,
|
||||
placeholder: `{{t("Value")}}`,
|
||||
useTypedConstant: true,
|
||||
changeOnSelect: true,
|
||||
},
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
add: {
|
||||
type: 'void',
|
||||
title: `{{t("Add parameter")}}`,
|
||||
'x-component': 'ArrayItems.Addition',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
onSubmit={({ url, params }) => {
|
||||
const componentProps = fieldSchema['x-component-props'] || {};
|
||||
componentProps.url = url;
|
||||
fieldSchema['x-component-props'] = componentProps;
|
||||
field.componentProps.url = url;
|
||||
componentProps.params = params;
|
||||
fieldSchema['x-component-props'] = componentProps;
|
||||
field.componentProps.params = params;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-component-props': componentProps,
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const customizeLinkActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:link',
|
||||
items: [
|
||||
{
|
||||
name: 'editButton',
|
||||
Component: ButtonEditor,
|
||||
useComponentProps() {
|
||||
const { buttonEditorProps } = useSchemaToolbar();
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'editLink',
|
||||
Component: SchemaSettingsActionLinkItem,
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useVisible() {
|
||||
const record = useCollectionRecord();
|
||||
return !_.isEmpty(record?.data);
|
||||
},
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
collectionName: name,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'remove',
|
||||
sort: 100,
|
||||
Component: RemoveButton as any,
|
||||
useComponentProps() {
|
||||
const { removeButtonProps } = useSchemaToolbar();
|
||||
return removeButtonProps;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
@ -8,11 +8,11 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
expect,
|
||||
expectSettingsMenu,
|
||||
oneDetailBlockWithM2oFieldToGeneral,
|
||||
oneEmptyDetailsBlock,
|
||||
test,
|
||||
expect,
|
||||
} from '@nocobase/test/e2e';
|
||||
import { detailBlockWithLinkageRule, detailsBlockWithLinkageRule } from './templatesOfBug';
|
||||
|
||||
@ -54,9 +54,9 @@ test.describe('multi data details block schema settings', () => {
|
||||
test('multi detail block support linkage rule', async ({ page, mockPage }) => {
|
||||
const nocoPage = await mockPage(detailsBlockWithLinkageRule).waitForInit();
|
||||
await nocoPage.goto();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-roles-details-roles.title')).not.toBeVisible();
|
||||
await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title')).not.toBeVisible();
|
||||
await page.getByRole('button', { name: 'right' }).click();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-roles-details-roles.title')).toBeVisible();
|
||||
await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -13,11 +13,13 @@ import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFi
|
||||
export const useDetailsWithPaginationBlockParams = (props) => {
|
||||
const { params } = props;
|
||||
|
||||
const { filter } = useParsedFilter({
|
||||
const { filter, parseVariableLoading } = useParsedFilter({
|
||||
filterOption: params?.filter,
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
const result = useMemo(() => {
|
||||
return { ...params, filter };
|
||||
}, [JSON.stringify(filter)]);
|
||||
|
||||
return { params: result, parseVariableLoading };
|
||||
};
|
||||
|
@ -13,7 +13,7 @@ import { useDetailsWithPaginationBlockParams } from './useDetailsWithPaginationB
|
||||
export function useDetailsWithPaginationDecoratorProps(props) {
|
||||
let parentRecord;
|
||||
|
||||
const params = useDetailsWithPaginationBlockParams(props);
|
||||
const { params, parseVariableLoading } = useDetailsWithPaginationBlockParams(props);
|
||||
|
||||
// association 的值是固定不变的,所以可以在条件中使用 hooks
|
||||
if (props.association) {
|
||||
@ -24,5 +24,9 @@ export function useDetailsWithPaginationDecoratorProps(props) {
|
||||
return {
|
||||
parentRecord,
|
||||
params,
|
||||
/**
|
||||
* 用于解析变量的 loading 状态
|
||||
*/
|
||||
parseVariableLoading,
|
||||
};
|
||||
}
|
||||
|
@ -72,6 +72,16 @@ const commonOptions = {
|
||||
Component: 'CustomRequestInitializer',
|
||||
useVisible: useVisibleCollection,
|
||||
},
|
||||
{
|
||||
name: 'link',
|
||||
title: '{{t("Link")}}',
|
||||
Component: 'LinkActionInitializer',
|
||||
useComponentProps() {
|
||||
return {
|
||||
'x-component': 'Action',
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -86,6 +86,16 @@ const commonOptions = {
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'link',
|
||||
title: '{{t("Link")}}',
|
||||
Component: 'LinkActionInitializer',
|
||||
useComponentProps() {
|
||||
return {
|
||||
'x-component': 'Action.Link',
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -11,7 +11,7 @@ import { useParentRecordCommon } from '../../../useParentRecordCommon';
|
||||
import { useGridCardBlockParams } from './useGridCardBlockParams';
|
||||
|
||||
export function useGridCardBlockDecoratorProps(props) {
|
||||
const params = useGridCardBlockParams(props);
|
||||
const { params, parseVariableLoading } = useGridCardBlockParams(props);
|
||||
let parentRecord;
|
||||
|
||||
// 因为 association 是固定的,所以可以在条件中使用 hooks
|
||||
@ -23,5 +23,7 @@ export function useGridCardBlockDecoratorProps(props) {
|
||||
return {
|
||||
params,
|
||||
parentRecord,
|
||||
/** 为 true 则表示正在解析 filter 参数中的变量 */
|
||||
parseVariableLoading,
|
||||
};
|
||||
}
|
||||
|
@ -7,12 +7,12 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFilter';
|
||||
import { useMemo } from 'react';
|
||||
import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFilter';
|
||||
|
||||
export function useGridCardBlockParams(props) {
|
||||
const { params } = props;
|
||||
const { filter: parsedFilter } = useParsedFilter({
|
||||
const { filter: parsedFilter, parseVariableLoading } = useParsedFilter({
|
||||
filterOption: params?.filter,
|
||||
});
|
||||
const paramsWithFilter = useMemo(() => {
|
||||
@ -22,5 +22,5 @@ export function useGridCardBlockParams(props) {
|
||||
};
|
||||
}, [parsedFilter, params]);
|
||||
|
||||
return paramsWithFilter;
|
||||
return { params: paramsWithFilter, parseVariableLoading };
|
||||
}
|
||||
|
@ -86,6 +86,16 @@ const commonOptions = {
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'link',
|
||||
title: '{{t("Link")}}',
|
||||
Component: 'LinkActionInitializer',
|
||||
useComponentProps() {
|
||||
return {
|
||||
'x-component': 'Action.Link',
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -261,6 +261,16 @@ const commonOptions = {
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'link',
|
||||
title: '{{t("Link")}}',
|
||||
Component: 'LinkActionInitializer',
|
||||
useComponentProps() {
|
||||
return {
|
||||
'x-component': 'Action.Link',
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -67,6 +67,20 @@ const commonOptions = {
|
||||
'x-align': 'right',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Link')}}",
|
||||
name: 'link',
|
||||
Component: 'LinkActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
},
|
||||
useComponentProps() {
|
||||
return {
|
||||
'x-component': 'Action',
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toggle',
|
||||
title: "{{t('Expand/Collapse')}}",
|
||||
|
@ -406,6 +406,8 @@ test.describe('actions schema settings', () => {
|
||||
.hover();
|
||||
await page.getByRole('menuitem', { name: 'Assign field values' }).click();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
if (!(await page.getByLabel('block-item-AssignedField-').getByRole('textbox').isVisible())) {
|
||||
await page.getByLabel('schema-initializer-Grid-assignFieldValuesForm:configureFields-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Nickname' }).click();
|
||||
|
@ -13,22 +13,23 @@ import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFi
|
||||
import { useParentRecordCommon } from '../../../useParentRecordCommon';
|
||||
|
||||
export const useTableBlockDecoratorProps = (props) => {
|
||||
const params = useTableBlockParams(props);
|
||||
const { params, parseVariableLoading } = useTableBlockParams(props);
|
||||
const parentRecord = useParentRecordCommon(props.association);
|
||||
|
||||
return {
|
||||
params,
|
||||
parentRecord,
|
||||
parseVariableLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export function useTableBlockParams(props) {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { filter: parsedFilter } = useParsedFilter({
|
||||
const { filter: parsedFilter, parseVariableLoading } = useParsedFilter({
|
||||
filterOption: props.params?.filter,
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
const params = useMemo(() => {
|
||||
const params = props.params || {};
|
||||
|
||||
// 1. sort
|
||||
@ -45,4 +46,6 @@ export function useTableBlockParams(props) {
|
||||
|
||||
return paramsWithFilter;
|
||||
}, [fieldSchema, parsedFilter, props.dragSort, props.params]);
|
||||
|
||||
return { params, parseVariableLoading };
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import { SchemaSettings } from '../../../../application/schema-settings/SchemaSe
|
||||
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager';
|
||||
import { useFieldComponentName } from '../../../../common/useFieldComponentName';
|
||||
import { EditOperator, useDesignable, useValidateSchema } from '../../../../schema-component';
|
||||
import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue';
|
||||
|
||||
export const filterFormItemFieldSettings = new SchemaSettings({
|
||||
name: 'fieldSettings:FilterFormItem',
|
||||
@ -184,6 +185,10 @@ export const filterFormItemFieldSettings = new SchemaSettings({
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'setDefaultValue',
|
||||
Component: SchemaSettingsDefaultValue,
|
||||
} as any,
|
||||
{
|
||||
name: 'setValidationRules',
|
||||
type: 'modal',
|
||||
|
@ -161,6 +161,15 @@ const MenuEditor = (props) => {
|
||||
return s;
|
||||
}, [data?.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMatchAdminName) {
|
||||
const s = findByUid(schema, defaultSelectedUid);
|
||||
if (s) {
|
||||
setTitle(s.title);
|
||||
}
|
||||
}
|
||||
}, [defaultSelectedUid, isMatchAdmin, isMatchAdminName, schema, setTitle]);
|
||||
|
||||
useRequest(
|
||||
{
|
||||
url: 'applicationPlugins:list',
|
||||
|
@ -1,15 +1,14 @@
|
||||
|
||||
|
||||
import {
|
||||
DetailsBlockProvider,
|
||||
ISchema,
|
||||
Plugin,
|
||||
SchemaComponent,
|
||||
useDetailsPaginationProps,
|
||||
useDetailsWithPaginationDecoratorProps,
|
||||
useDetailsWithPaginationProps,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { mockApp } from '@nocobase/client/demo-utils';
|
||||
import { SchemaComponent, Plugin } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
@ -52,28 +51,30 @@ const schema: ISchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const Demo = () => {
|
||||
return <SchemaComponent
|
||||
schema={schema}
|
||||
scope={{
|
||||
useDetailsWithPaginationDecoratorProps,
|
||||
useDetailsWithPaginationProps,
|
||||
useDetailsPaginationProps,
|
||||
}}
|
||||
/>;
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
scope={{
|
||||
useDetailsWithPaginationDecoratorProps,
|
||||
useDetailsWithPaginationProps,
|
||||
useDetailsPaginationProps,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
class DemoPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.router.add('root', { path: '/', Component: Demo })
|
||||
this.app.router.add('root', { path: '/', Component: Demo });
|
||||
}
|
||||
}
|
||||
|
||||
const app = mockApp({
|
||||
plugins: [DemoPlugin],
|
||||
components: { DetailsBlockProvider }
|
||||
components: { DetailsBlockProvider },
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
||||
|
@ -1,15 +1,14 @@
|
||||
|
||||
|
||||
import {
|
||||
DetailsBlockProvider,
|
||||
ISchema,
|
||||
Plugin,
|
||||
SchemaComponent,
|
||||
useDetailsPaginationProps,
|
||||
useDetailsWithPaginationDecoratorProps,
|
||||
useDetailsWithPaginationProps,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { mockApp } from '@nocobase/client/demo-utils';
|
||||
import { SchemaComponent, Plugin } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
@ -47,25 +46,27 @@ const schema: ISchema = {
|
||||
};
|
||||
|
||||
const Demo = () => {
|
||||
return <SchemaComponent
|
||||
schema={schema}
|
||||
scope={{
|
||||
useDetailsWithPaginationDecoratorProps,
|
||||
useDetailsWithPaginationProps,
|
||||
useDetailsPaginationProps,
|
||||
}}
|
||||
/>;
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
scope={{
|
||||
useDetailsWithPaginationDecoratorProps,
|
||||
useDetailsWithPaginationProps,
|
||||
useDetailsPaginationProps,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
class DemoPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.router.add('root', { path: '/', Component: Demo })
|
||||
this.app.router.add('root', { path: '/', Component: Demo });
|
||||
}
|
||||
}
|
||||
|
||||
const app = mockApp({
|
||||
plugins: [DemoPlugin],
|
||||
components: { DetailsBlockProvider }
|
||||
components: { DetailsBlockProvider },
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
||||
|
@ -13,7 +13,6 @@ import { uid } from '@nocobase/utils/client';
|
||||
import _ from 'lodash';
|
||||
import { ActionType } from '../../../schema-settings/LinkageRules/type';
|
||||
import { VariableOption, VariablesContextType } from '../../../variables/types';
|
||||
import { REGEX_OF_VARIABLE } from '../../../variables/utils/isVariable';
|
||||
import { conditionAnalyses } from '../../common/utils/uitls';
|
||||
|
||||
interface Props {
|
||||
@ -138,7 +137,7 @@ export const collectFieldStateOfLinkageRules = ({
|
||||
}
|
||||
};
|
||||
|
||||
async function replaceVariables(
|
||||
export async function replaceVariables(
|
||||
value: string,
|
||||
{
|
||||
variables,
|
||||
@ -155,6 +154,7 @@ async function replaceVariables(
|
||||
return;
|
||||
}
|
||||
|
||||
const REGEX_OF_VARIABLE = /{\{\s*([a-zA-Z0-9_$-.]+?)\s*\}\}/g;
|
||||
const waitForParsing = value.match(REGEX_OF_VARIABLE)?.map(async (item) => {
|
||||
const result = await variables.parseVariable(item, localVariables);
|
||||
|
||||
@ -169,7 +169,6 @@ async function replaceVariables(
|
||||
if (waitForParsing) {
|
||||
await Promise.all(waitForParsing);
|
||||
}
|
||||
|
||||
return {
|
||||
exp: value.replace(REGEX_OF_VARIABLE, (match) => {
|
||||
return `{{${store[match] || match}}}`;
|
||||
|
@ -62,7 +62,7 @@ const useCompatGridCardBlockParams = (props) => {
|
||||
|
||||
// 因为 x-use-decorator-props 的值是固定的,所以可以在条件中使用 hooks
|
||||
if (schema['x-use-decorator-props']) {
|
||||
return props.params;
|
||||
return { params: props.params, parseVariableLoading: props.parseVariableLoading };
|
||||
} else {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useGridCardBlockParams(props);
|
||||
@ -70,7 +70,11 @@ const useCompatGridCardBlockParams = (props) => {
|
||||
};
|
||||
|
||||
export const GridCardBlockProvider = withDynamicSchemaProps((props) => {
|
||||
const params = useCompatGridCardBlockParams(props);
|
||||
const { params, parseVariableLoading } = useCompatGridCardBlockParams(props);
|
||||
|
||||
if (parseVariableLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockProvider name="grid-card" {...props} params={params}>
|
||||
|
@ -65,7 +65,7 @@ const InternalListBlockProvider = (props) => {
|
||||
|
||||
export const ListBlockProvider = withDynamicSchemaProps((props) => {
|
||||
const { params } = props;
|
||||
const { filter: parsedFilter } = useParsedFilter({
|
||||
const { filter: parsedFilter, parseVariableLoading } = useParsedFilter({
|
||||
filterOption: params?.filter,
|
||||
});
|
||||
const paramsWithFilter = useMemo(() => {
|
||||
@ -77,7 +77,7 @@ export const ListBlockProvider = withDynamicSchemaProps((props) => {
|
||||
|
||||
// parse filter 的过程是异步的,且一开始 parsedFilter 是一个空对象,所以当 parsedFilter 为空 params.filter 不为空时,
|
||||
// 说明 filter 还未解析完成,此时不应该渲染,防止重复请求多次
|
||||
if (_.isEmpty(parsedFilter) && !_.isEmpty(params?.filter)) {
|
||||
if ((_.isEmpty(parsedFilter) && !_.isEmpty(params?.filter)) || parseVariableLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -285,8 +285,9 @@ const SideMenu = ({
|
||||
mode,
|
||||
sideMenuSchema,
|
||||
sideMenuRef,
|
||||
defaultOpenKeys,
|
||||
defaultSelectedKeys,
|
||||
openKeys,
|
||||
setOpenKeys,
|
||||
selectedKeys,
|
||||
onSelect,
|
||||
render,
|
||||
t,
|
||||
@ -344,11 +345,14 @@ const SideMenu = ({
|
||||
<Component />
|
||||
<AntdMenu
|
||||
mode={'inline'}
|
||||
defaultOpenKeys={defaultOpenKeys}
|
||||
defaultSelectedKeys={defaultSelectedKeys}
|
||||
openKeys={openKeys}
|
||||
selectedKeys={selectedKeys}
|
||||
onSelect={(info) => {
|
||||
onSelect?.(info);
|
||||
}}
|
||||
onOpenChange={(openKeys) => {
|
||||
setOpenKeys(openKeys);
|
||||
}}
|
||||
className={sideMenuClass}
|
||||
items={items as MenuProps['items']}
|
||||
/>
|
||||
@ -474,8 +478,9 @@ export const Menu: ComposedMenu = observer(
|
||||
mode={mode}
|
||||
sideMenuSchema={sideMenuSchema}
|
||||
sideMenuRef={sideMenuRef}
|
||||
defaultOpenKeys={defaultOpenKeys}
|
||||
defaultSelectedKeys={defaultSelectedKeys}
|
||||
openKeys={defaultOpenKeys}
|
||||
setOpenKeys={setDefaultOpenKeys}
|
||||
selectedKeys={selectedKeys}
|
||||
onSelect={onSelect}
|
||||
render={render}
|
||||
t={t}
|
||||
|
@ -17,7 +17,7 @@ import { useAPIClient } from '../../../api-client';
|
||||
import { useFormBlockContext, useTableBlockContext } from '../../../block-provider';
|
||||
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager';
|
||||
import { useSortFields } from '../../../collection-manager/action-hooks';
|
||||
import { FilterBlockType, mergeFilter } from '../../../filter-provider/utils';
|
||||
import { FilterBlockType } from '../../../filter-provider/utils';
|
||||
import { SetDataLoadingMode } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
|
||||
import {
|
||||
GeneralSchemaDesigner,
|
||||
@ -113,17 +113,14 @@ export const TableBlockDesigner = () => {
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
fieldSchema['x-decorator-props']['params'] = params;
|
||||
const filters = service.params?.[1]?.filters || {};
|
||||
service.run(
|
||||
{ ...service.params?.[0], filter: mergeFilter([...Object.values(filters), filter]), page: 1 },
|
||||
{ filters },
|
||||
);
|
||||
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
service.params[0].page = 1;
|
||||
},
|
||||
[dn, field.decoratorProps, fieldSchema, service],
|
||||
);
|
||||
|
@ -221,7 +221,16 @@ export function Input(props: VariableInputProps) {
|
||||
const loadData = async (selectedOptions: DefaultOptionType[]) => {
|
||||
const option = selectedOptions[selectedOptions.length - 1];
|
||||
if (!option.children?.length && !option.isLeaf && option.loadChildren) {
|
||||
await option.loadChildren(option);
|
||||
let activeKey;
|
||||
if (variable && variable.length >= 2) {
|
||||
for (const key of variable) {
|
||||
if (key === option[names.value]) {
|
||||
activeKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
await option.loadChildren(option, activeKey, variable);
|
||||
setOptions((prev) => [...prev]);
|
||||
}
|
||||
};
|
||||
@ -260,7 +269,7 @@ export function Input(props: VariableInputProps) {
|
||||
prevOption = options.find((item) => item[names.value] === key);
|
||||
} else {
|
||||
if (prevOption.loadChildren && !prevOption.children?.length) {
|
||||
await prevOption.loadChildren(prevOption);
|
||||
await prevOption.loadChildren(prevOption, key, variable);
|
||||
}
|
||||
prevOption = prevOption.children.find((item) => item[names.value] === key);
|
||||
}
|
||||
|
@ -443,13 +443,7 @@ export function TextArea(props) {
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
{!disabled ? (
|
||||
<VariableSelect
|
||||
className=""
|
||||
options={options}
|
||||
setOptions={setOptions}
|
||||
onInsert={onInsert}
|
||||
changeOnSelect={changeOnSelect}
|
||||
/>
|
||||
<VariableSelect options={options} setOptions={setOptions} onInsert={onInsert} changeOnSelect={changeOnSelect} />
|
||||
) : null}
|
||||
</Space.Compact>,
|
||||
);
|
||||
@ -477,7 +471,7 @@ async function preloadOptions(scope, value: string) {
|
||||
prevOption = options.find((item) => item.value === key);
|
||||
} else {
|
||||
if (prevOption.loadChildren && !prevOption.children?.length) {
|
||||
await prevOption.loadChildren(prevOption);
|
||||
await prevOption.loadChildren(prevOption, key, keys);
|
||||
}
|
||||
prevOption = prevOption.children.find((item) => item.value === key);
|
||||
}
|
||||
|
@ -22,6 +22,13 @@ export function VariableSelect({
|
||||
changeOnSelect = false,
|
||||
fieldNames = {},
|
||||
className,
|
||||
}: {
|
||||
options: any[];
|
||||
setOptions: (options: any) => void;
|
||||
onInsert: (keyPaths: string[]) => void;
|
||||
changeOnSelect?: boolean;
|
||||
fieldNames?: any;
|
||||
className?: string;
|
||||
}): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const [selectedVar, setSelectedVar] = useState<string[]>([]);
|
||||
@ -50,7 +57,7 @@ export function VariableSelect({
|
||||
return;
|
||||
}
|
||||
const option = selectedOptions[selectedOptions.length - 1];
|
||||
if (!option?.children?.length) {
|
||||
if ((!option?.children?.length && !option?.loadChildren) || option?.isLeaf) {
|
||||
onInsert(keyPaths);
|
||||
}
|
||||
}}
|
||||
|
@ -28,6 +28,8 @@ import { CreateSubmitActionInitializer } from '../modules/actions/submit/CreateS
|
||||
import { UpdateSubmitActionInitializer } from '../modules/actions/submit/UpdateSubmitActionInitializer';
|
||||
import { UpdateRecordActionInitializer } from '../modules/actions/update-record/UpdateRecordActionInitializer';
|
||||
import { PopupActionInitializer } from '../modules/actions/view-edit-popup/PopupActionInitializer';
|
||||
import { LinkActionInitializer } from '../modules/actions/link/LinkActionInitializer';
|
||||
|
||||
import { recordFormBlockInitializers } from '../modules/actions/view-edit-popup/RecordFormBlockInitializers';
|
||||
import { UpdateActionInitializer } from '../modules/actions/view-edit-popup/UpdateActionInitializer';
|
||||
import { ViewActionInitializer } from '../modules/actions/view-edit-popup/ViewActionInitializer';
|
||||
@ -171,6 +173,7 @@ export class SchemaInitializerPlugin extends Plugin {
|
||||
ViewActionInitializer,
|
||||
UpdateActionInitializer,
|
||||
PopupActionInitializer,
|
||||
LinkActionInitializer,
|
||||
UpdateRecordActionInitializer,
|
||||
CreateSubmitActionInitializer,
|
||||
UpdateSubmitActionInitializer,
|
||||
|
@ -22,6 +22,8 @@ import { createSubmitActionSettings } from '../modules/actions/submit/createSubm
|
||||
import { submitActionSettings, updateSubmitActionSettings } from '../modules/actions/submit/updateSubmitActionSettings';
|
||||
import { customizeUpdateRecordActionSettings } from '../modules/actions/update-record/customizeUpdateRecordActionSettings';
|
||||
import { customizePopupActionSettings } from '../modules/actions/view-edit-popup/customizePopupActionSettings';
|
||||
import { customizeLinkActionSettings } from '../modules/actions/link/customizeLinkActionSettings';
|
||||
|
||||
import { editActionSettings } from '../modules/actions/view-edit-popup/editActionSettings';
|
||||
import { viewActionSettings } from '../modules/actions/view-edit-popup/viewActionSettings';
|
||||
import {
|
||||
@ -88,6 +90,7 @@ export class SchemaSettingsPlugin extends Plugin {
|
||||
this.schemaSettingsManager.add(bulkDeleteActionSettings);
|
||||
this.schemaSettingsManager.add(customizeAddRecordActionSettings);
|
||||
this.schemaSettingsManager.add(customizePopupActionSettings);
|
||||
this.schemaSettingsManager.add(customizeLinkActionSettings);
|
||||
this.schemaSettingsManager.add(customizeUpdateRecordActionSettings);
|
||||
this.schemaSettingsManager.add(createSubmitActionSettings);
|
||||
this.schemaSettingsManager.add(updateSubmitActionSettings);
|
||||
|
@ -197,12 +197,12 @@ export const getShouldChange = ({
|
||||
if (['o2o', 'o2m', 'oho'].includes(collectionFieldOfVariable?.interface)) {
|
||||
return false;
|
||||
}
|
||||
if (!collectionField.target && collectionFieldOfVariable?.target) {
|
||||
return false;
|
||||
}
|
||||
if (collectionField.target && !collectionFieldOfVariable?.target) {
|
||||
return false;
|
||||
}
|
||||
// if (!collectionField.target && collectionFieldOfVariable?.target) {
|
||||
// return false;
|
||||
// }
|
||||
// if (collectionField.target && !collectionFieldOfVariable?.target) {
|
||||
// return false;
|
||||
// }
|
||||
if (
|
||||
collectionField.target &&
|
||||
collectionFieldOfVariable?.target &&
|
||||
|
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { isObservable } from '@formily/reactive';
|
||||
import { renderHook } from '@nocobase/test/client';
|
||||
import {
|
||||
getURLSearchParams,
|
||||
getURLSearchParamsChildren,
|
||||
useURLSearchParamsCtx,
|
||||
} from '../../hooks/useURLSearchParamsVariable';
|
||||
|
||||
test('getURLSearchParamsChildren should return an array of options with expected properties', () => {
|
||||
const queryParams = {
|
||||
param1: 'value1',
|
||||
param2: 'value2',
|
||||
param3: 'value3',
|
||||
};
|
||||
|
||||
const result = getURLSearchParamsChildren(queryParams);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
label: 'param1',
|
||||
value: 'param1',
|
||||
key: 'param1',
|
||||
isLeaf: true,
|
||||
},
|
||||
{
|
||||
label: 'param2',
|
||||
value: 'param2',
|
||||
key: 'param2',
|
||||
isLeaf: true,
|
||||
},
|
||||
{
|
||||
label: 'param3',
|
||||
value: 'param3',
|
||||
key: 'param3',
|
||||
isLeaf: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('getURLSearchParams should parse search string and return params object', () => {
|
||||
const search = '?param1=value1¶m2=value2¶m3=value3';
|
||||
const expectedParams = {
|
||||
param1: 'value1',
|
||||
param2: 'value2',
|
||||
param3: 'value3',
|
||||
};
|
||||
|
||||
const result = getURLSearchParams(search);
|
||||
|
||||
expect(result).toEqual(expectedParams);
|
||||
});
|
||||
|
||||
test('useURLSearchParamsCtx should return the parsed search params object', () => {
|
||||
const search = '?param1=value1¶m2=value2¶m3=value3';
|
||||
const { result } = renderHook(() => useURLSearchParamsCtx(search));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
param1: 'value1',
|
||||
param2: 'value2',
|
||||
param3: 'value3',
|
||||
});
|
||||
expect(isObservable(result.current)).toBe(true);
|
||||
});
|
||||
|
||||
test('useURLSearchParamsCtx should update the parsed search params object when search value changes', () => {
|
||||
const { result, rerender } = renderHook(({ search }) => useURLSearchParamsCtx(search), {
|
||||
initialProps: {
|
||||
search: '?param1=value1¶m2=value2¶m3=value3',
|
||||
},
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
param1: 'value1',
|
||||
param2: 'value2',
|
||||
param3: 'value3',
|
||||
});
|
||||
|
||||
rerender({
|
||||
search: '?param1=newValue1¶m2=newValue2',
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
param1: 'newValue1',
|
||||
param2: 'newValue2',
|
||||
});
|
||||
|
||||
rerender({
|
||||
search: '',
|
||||
});
|
||||
expect(result.current).toEqual({});
|
||||
});
|
@ -36,7 +36,14 @@ interface GetOptionsParams {
|
||||
* 不需要禁用选项,一般会在表达式中使用
|
||||
*/
|
||||
noDisabled?: boolean;
|
||||
loadChildren?: (option: Option) => Promise<void>;
|
||||
/**
|
||||
* 加载选项的 children
|
||||
* @param option 需要加载 children 的选项
|
||||
* @param activeKey 当前选项所对应的 key
|
||||
* @param variablePath 变量路径数组,如 ['$user', 'nickname']
|
||||
* @returns
|
||||
*/
|
||||
loadChildren?: (option: Option, activeKey?: string, variablePath?: string[]) => Promise<void>;
|
||||
compile: (value: string) => any;
|
||||
isDisabled?: (params: IsDisabledParams) => boolean;
|
||||
getCollectionField?: (name: string) => CollectionFieldOptions_deprecated;
|
||||
@ -153,6 +160,28 @@ const getChildren = (
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getLabelWithTooltip = (title: string, tooltip?: string) => {
|
||||
return tooltip ? (
|
||||
<Tooltip placement="left" title={tooltip} zIndex={9999}>
|
||||
<span
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
marginLeft: -14,
|
||||
paddingLeft: 14,
|
||||
marginRight: -80,
|
||||
paddingRight: 80,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
title
|
||||
);
|
||||
};
|
||||
|
||||
export const useBaseVariable = ({
|
||||
collectionField,
|
||||
uiSchema,
|
||||
@ -234,25 +263,7 @@ export const useBaseVariable = ({
|
||||
|
||||
const result = useMemo(() => {
|
||||
return {
|
||||
label: tooltip ? (
|
||||
<Tooltip placement="left" title={tooltip} zIndex={9999}>
|
||||
<span
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
marginLeft: -14,
|
||||
paddingLeft: 14,
|
||||
marginRight: -80,
|
||||
paddingRight: 80,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
title
|
||||
),
|
||||
label: getLabelWithTooltip(title, tooltip),
|
||||
value: name,
|
||||
key: name,
|
||||
isLeaf: noChildren,
|
||||
|
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { observable, untracked } from '@formily/reactive';
|
||||
import _ from 'lodash';
|
||||
import qs from 'qs';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useFlag } from '../../../flag-provider/hooks/useFlag';
|
||||
import { Option } from '../type';
|
||||
import { getLabelWithTooltip } from './useBaseVariable';
|
||||
|
||||
export const getURLSearchParams = (search: string) => {
|
||||
if (search.startsWith('?')) {
|
||||
search = search.slice(1);
|
||||
}
|
||||
const params = qs.parse(search);
|
||||
return params || {};
|
||||
};
|
||||
|
||||
export const getURLSearchParamsChildren = (queryParams: Record<string, any>): Option[] => {
|
||||
return Object.keys(queryParams).map((key) => {
|
||||
return {
|
||||
label: key,
|
||||
value: key,
|
||||
key,
|
||||
isLeaf: true,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const useURLSearchParamsCtx = (search: string) => {
|
||||
// 使用响应式对象,目的是为了在变量值变化时,能够触发重新解析变量值
|
||||
const [_urlSearchParamsCtx] = useState(() => observable({}));
|
||||
|
||||
return useMemo(() => {
|
||||
const newValue = getURLSearchParams(search);
|
||||
|
||||
untracked(() => {
|
||||
Object.assign(_urlSearchParamsCtx, newValue);
|
||||
Object.keys(_urlSearchParamsCtx).forEach((key) => {
|
||||
if (newValue[key] === undefined) {
|
||||
delete _urlSearchParamsCtx[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return _urlSearchParamsCtx;
|
||||
}, [_urlSearchParamsCtx, search]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 变量:`URL search params`
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export const useURLSearchParamsVariable = (props: any = {}) => {
|
||||
const variableName = '$nURLSearchParams';
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const { isVariableParsedInOtherContext } = useFlag();
|
||||
const urlSearchParamsCtx = useURLSearchParamsCtx(location.search);
|
||||
const disabled = useMemo(() => _.isEmpty(urlSearchParamsCtx), [urlSearchParamsCtx]);
|
||||
const urlSearchParamsSettings: Option = useMemo(() => {
|
||||
return {
|
||||
label: getLabelWithTooltip(
|
||||
t('URL search params'),
|
||||
disabled
|
||||
? t(
|
||||
'The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.',
|
||||
)
|
||||
: '',
|
||||
),
|
||||
value: variableName,
|
||||
key: variableName,
|
||||
isLeaf: false,
|
||||
disabled,
|
||||
loadChildren: async (option, activeKey) => {
|
||||
const activeSettings = activeKey
|
||||
? {
|
||||
[activeKey]: undefined,
|
||||
}
|
||||
: {};
|
||||
option.children = getURLSearchParamsChildren({ ...activeSettings, ...urlSearchParamsCtx });
|
||||
},
|
||||
};
|
||||
}, [disabled, t, urlSearchParamsCtx]);
|
||||
|
||||
return {
|
||||
name: variableName,
|
||||
/** 变量配置 */
|
||||
urlSearchParamsSettings,
|
||||
/** 变量值 */
|
||||
urlSearchParamsCtx,
|
||||
shouldDisplay: !isVariableParsedInOtherContext,
|
||||
};
|
||||
};
|
@ -18,6 +18,7 @@ import { useCurrentParentRecordVariable } from './useParentRecordVariable';
|
||||
import { usePopupVariable } from './usePopupVariable';
|
||||
import { useCurrentRecordVariable } from './useRecordVariable';
|
||||
import { useCurrentRoleVariable } from './useRoleVariable';
|
||||
import { useURLSearchParamsVariable } from './useURLSearchParamsVariable';
|
||||
import { useCurrentUserVariable } from './useUserVariable';
|
||||
|
||||
interface Props {
|
||||
@ -102,6 +103,7 @@ export const useVariableOptions = ({
|
||||
noDisabled,
|
||||
targetFieldSchema,
|
||||
});
|
||||
const { urlSearchParamsSettings, shouldDisplay: shouldDisplayURLSearchParams } = useURLSearchParamsVariable();
|
||||
|
||||
return useMemo(() => {
|
||||
return [
|
||||
@ -113,6 +115,7 @@ export const useVariableOptions = ({
|
||||
shouldDisplayCurrentRecord && currentRecordSettings,
|
||||
shouldDisplayCurrentParentRecord && currentParentRecordSettings,
|
||||
shouldDisplayPopupRecord && popupRecordSettings,
|
||||
shouldDisplayURLSearchParams && urlSearchParamsSettings,
|
||||
].filter(Boolean);
|
||||
}, [
|
||||
currentUserSettings,
|
||||
@ -128,5 +131,7 @@ export const useVariableOptions = ({
|
||||
currentParentRecordSettings,
|
||||
shouldDisplayPopupRecord,
|
||||
popupRecordSettings,
|
||||
shouldDisplayURLSearchParams,
|
||||
urlSearchParamsSettings,
|
||||
]);
|
||||
};
|
||||
|
@ -19,8 +19,14 @@ export interface Option extends DefaultOptionType {
|
||||
// 标记是否为叶子节点,设置了 `loadData` 时有效
|
||||
// 设为 `false` 时会强制标记为父节点,即使当前节点没有 children,也会显示展开图标
|
||||
isLeaf?: boolean;
|
||||
/** 当开启异步加载时有效,用于加载当前 node 的 children */
|
||||
loadChildren?(option: Option): Promise<void>;
|
||||
/**
|
||||
* 当开启异步加载时有效,用于加载当前 node 的 children
|
||||
* @param option 需要加载 children 的选项
|
||||
* @param activeKey 当前选项所对应的 key
|
||||
* @param variablePath 变量路径数组,如 ['$user', 'nickname']
|
||||
* @returns
|
||||
*/
|
||||
loadChildren?: (option: Option, activeKey?: string, variablePath?: string[]) => Promise<void>;
|
||||
/** 变量中的字段 */
|
||||
field?: FieldOption;
|
||||
depth?: number;
|
||||
|
@ -63,7 +63,7 @@ const useParseDataScopeFilter = ({ exclude = defaultExclude }: Props = {}) => {
|
||||
Object.keys(flat).map(async (key) => {
|
||||
flat[key] = await flat[key];
|
||||
if (flat[key] === undefined) {
|
||||
flat[key] = null;
|
||||
delete flat[key];
|
||||
}
|
||||
return flat[key];
|
||||
}),
|
||||
|
@ -10,6 +10,7 @@
|
||||
import { SchemaExpressionScopeContext, SchemaOptionsContext } from '@formily/react';
|
||||
import { act, renderHook, waitFor } from '@nocobase/test/client';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router';
|
||||
import { APIClientProvider } from '../../api-client';
|
||||
import { mockAPIClient } from '../../testUtils';
|
||||
import { CurrentUserProvider } from '../../user';
|
||||
@ -142,15 +143,17 @@ mockRequest.onGet('/someBelongsToField/0/belongsToField:get').reply(() => {
|
||||
|
||||
const Providers = ({ children }) => {
|
||||
return (
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<CurrentUserProvider>
|
||||
<SchemaOptionsContext.Provider value={{}}>
|
||||
<SchemaExpressionScopeContext.Provider value={{}}>
|
||||
<VariablesProvider>{children}</VariablesProvider>
|
||||
</SchemaExpressionScopeContext.Provider>
|
||||
</SchemaOptionsContext.Provider>
|
||||
</CurrentUserProvider>
|
||||
</APIClientProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<CurrentUserProvider>
|
||||
<SchemaOptionsContext.Provider value={{}}>
|
||||
<SchemaExpressionScopeContext.Provider value={{}}>
|
||||
<VariablesProvider>{children}</VariablesProvider>
|
||||
</SchemaExpressionScopeContext.Provider>
|
||||
</SchemaOptionsContext.Provider>
|
||||
</CurrentUserProvider>
|
||||
</APIClientProvider>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
@ -218,6 +221,7 @@ describe('useVariables', () => {
|
||||
"yesterday": [Function],
|
||||
},
|
||||
"$nRole": "root",
|
||||
"$nURLSearchParams": {},
|
||||
"$system": {
|
||||
"now": [Function],
|
||||
},
|
||||
@ -429,6 +433,7 @@ describe('useVariables', () => {
|
||||
"yesterday": [Function],
|
||||
},
|
||||
"$nRole": "root",
|
||||
"$nURLSearchParams": {},
|
||||
"$system": {
|
||||
"now": [Function],
|
||||
},
|
||||
@ -513,6 +518,7 @@ describe('useVariables', () => {
|
||||
"yesterday": [Function],
|
||||
},
|
||||
"$nRole": "root",
|
||||
"$nURLSearchParams": {},
|
||||
"$new": {
|
||||
"name": "new variable",
|
||||
},
|
||||
|
@ -36,3 +36,7 @@ it('should return true for a string with a valid variable "{{ $nRecord._name }}"
|
||||
it('should return true for a string with a valid variable " {{ $nRecord.name }} "', () => {
|
||||
expect(isVariable(' {{ $nRecord.name }} ')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a string with a valid variable "{{ $nRecord.name }}+1"', () => {
|
||||
expect(isVariable('{{ $nRecord.name }}+1')).toBe(false);
|
||||
});
|
||||
|
@ -7,4 +7,4 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export const DEBOUNCE_WAIT = 300;
|
||||
export const DEBOUNCE_WAIT = 100;
|
||||
|
@ -12,12 +12,18 @@ import { useMemo } from 'react';
|
||||
import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager';
|
||||
import { useCurrentUserVariable, useDatetimeVariable } from '../../schema-settings';
|
||||
import { useCurrentRoleVariable } from '../../schema-settings/VariableInput/hooks/useRoleVariable';
|
||||
import { useURLSearchParamsVariable } from '../../schema-settings/VariableInput/hooks/useURLSearchParamsVariable';
|
||||
import { VariableOption } from '../types';
|
||||
|
||||
/**
|
||||
* 相当于全局的变量
|
||||
* @returns
|
||||
*/
|
||||
const useBuiltInVariables = () => {
|
||||
const { currentUserCtx } = useCurrentUserVariable();
|
||||
const { currentRoleCtx } = useCurrentRoleVariable();
|
||||
const { datetimeCtx } = useDatetimeVariable();
|
||||
const { urlSearchParamsCtx, name: urlSearchParamsName } = useURLSearchParamsVariable();
|
||||
const builtinVariables: VariableOption[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
@ -71,8 +77,12 @@ const useBuiltInVariables = () => {
|
||||
name: 'currentTime',
|
||||
ctx: () => dayjs().toISOString(),
|
||||
},
|
||||
{
|
||||
name: urlSearchParamsName,
|
||||
ctx: urlSearchParamsCtx,
|
||||
},
|
||||
];
|
||||
}, [currentRoleCtx, currentUserCtx, datetimeCtx]);
|
||||
}, [currentRoleCtx, currentUserCtx, datetimeCtx, urlSearchParamsCtx, urlSearchParamsName]);
|
||||
|
||||
return { builtinVariables };
|
||||
};
|
||||
|
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export const REGEX_OF_VARIABLE = /\{\{\s*([a-zA-Z0-9_$-.]+?)\s*\}\}/g;
|
||||
export const REGEX_OF_VARIABLE = /^\s*\{\{\s*([a-zA-Z0-9_$-.]+?)\s*\}\}\s*$/g;
|
||||
|
||||
export const isVariable = (str: unknown) => {
|
||||
if (typeof str !== 'string') {
|
||||
|
@ -7,8 +7,8 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { evaluate } from '../../utils';
|
||||
import evaluators from '..';
|
||||
import { evaluate } from '../../utils';
|
||||
|
||||
describe('evaluate', () => {
|
||||
describe('pre-process', () => {
|
||||
|
@ -8,22 +8,22 @@
|
||||
*/
|
||||
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
SchemaSettings,
|
||||
SchemaSettingsBlockTitleItem,
|
||||
SchemaSettingsSelectItem,
|
||||
useCollection,
|
||||
SchemaSettingsSwitchItem,
|
||||
SchemaSettingsDataScope,
|
||||
useDesignable,
|
||||
SchemaSettingsCascaderItem,
|
||||
useFormBlockContext,
|
||||
removeNullCondition,
|
||||
SchemaSettingsTemplate,
|
||||
useCollectionManager_deprecated,
|
||||
SchemaSettingsBlockHeightItem,
|
||||
SchemaSettingsBlockTitleItem,
|
||||
SchemaSettingsCascaderItem,
|
||||
SchemaSettingsDataScope,
|
||||
SchemaSettingsSelectItem,
|
||||
SchemaSettingsSwitchItem,
|
||||
SchemaSettingsTemplate,
|
||||
removeNullCondition,
|
||||
useCollection,
|
||||
useCollectionManager_deprecated,
|
||||
useDesignable,
|
||||
useFormBlockContext,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '../../locale';
|
||||
import { useCalendarBlockContext } from '../schema-initializer/CalendarBlockProvider';
|
||||
|
||||
@ -181,7 +181,6 @@ export const calendarBlockSettings = new SchemaSettings({
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { form } = useFormBlockContext();
|
||||
const field = useField();
|
||||
const { service } = useCalendarBlockContext();
|
||||
const { dn } = useDesignable();
|
||||
return {
|
||||
collectionName: name,
|
||||
@ -193,7 +192,6 @@ export const calendarBlockSettings = new SchemaSettings({
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
fieldSchema['x-decorator-props']['params'] = params;
|
||||
service.run({ ...service?.params?.[0], filter });
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
|
@ -11,7 +11,7 @@ import { useParentRecordCommon } from '@nocobase/client';
|
||||
import { useCalendarBlockParams } from './useCalendarBlockParams';
|
||||
|
||||
export function useCalendarBlockDecoratorProps(props) {
|
||||
const params = useCalendarBlockParams(props);
|
||||
const { params, parseVariableLoading } = useCalendarBlockParams(props);
|
||||
let parentRecord;
|
||||
|
||||
// 因为 association 是一个固定的值,所以可以在 hooks 中直接使用
|
||||
@ -23,5 +23,9 @@ export function useCalendarBlockDecoratorProps(props) {
|
||||
return {
|
||||
params,
|
||||
parentRecord,
|
||||
/**
|
||||
* 为 true 则表示正在解析 filter 参数中的变量
|
||||
*/
|
||||
parseVariableLoading,
|
||||
};
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { useParsedFilter } from '@nocobase/client';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useCalendarBlockParams(props) {
|
||||
const { filter } = useParsedFilter({
|
||||
const { filter, parseVariableLoading } = useParsedFilter({
|
||||
filterOption: props.params?.filter,
|
||||
});
|
||||
const appends = useMemo(() => {
|
||||
@ -29,7 +29,9 @@ export function useCalendarBlockParams(props) {
|
||||
return arr;
|
||||
}, [props.fieldNames]);
|
||||
|
||||
return useMemo(() => {
|
||||
const params = useMemo(() => {
|
||||
return { ...props.params, appends: [...appends, ...(props.params.appends || [])], paginate: false, filter };
|
||||
}, [appends, JSON.stringify(filter), props.params]);
|
||||
|
||||
return { params, parseVariableLoading };
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ const useCompatCalendarBlockParams = (props) => {
|
||||
|
||||
// 因为 x-use-decorator-props 的值是固定不变的,所以可以在条件中使用 hooks
|
||||
if (fieldSchema['x-use-decorator-props']) {
|
||||
return props.params;
|
||||
return { params: props.params, parseVariableLoading: props.parseVariableLoading };
|
||||
} else {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useCalendarBlockParams(props);
|
||||
@ -53,7 +53,12 @@ const useCompatCalendarBlockParams = (props) => {
|
||||
|
||||
export const CalendarBlockProvider = withDynamicSchemaProps(
|
||||
(props) => {
|
||||
const params = useCompatCalendarBlockParams(props);
|
||||
const { params, parseVariableLoading } = useCompatCalendarBlockParams(props);
|
||||
|
||||
if (parseVariableLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockProvider name="calendar" {...props} params={params}>
|
||||
<InternalCalendarBlockProvider {...props} />
|
||||
|
@ -7,3 +7,35 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { expectSettingsMenu, test } from '@nocobase/test/e2e';
|
||||
|
||||
test.describe('form item & filter form', () => {
|
||||
test('supported options', async ({ page, mockPage }) => {
|
||||
await mockPage().goto();
|
||||
|
||||
// 在页面上创建一个筛选表单,并在表单中添加一个字段
|
||||
await page.getByLabel('schema-initializer-Grid-page:').hover();
|
||||
await page.getByRole('menuitem', { name: 'form Form right' }).nth(1).click();
|
||||
await page.getByRole('menuitem', { name: 'Users' }).click();
|
||||
await page.getByLabel('schema-initializer-Grid-filterForm:configureFields-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Nickname' }).click();
|
||||
|
||||
await expectSettingsMenu({
|
||||
page,
|
||||
showMenu: async () => {
|
||||
await page.getByLabel('block-item-CollectionField-').hover();
|
||||
await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FilterFormItem-users-').hover();
|
||||
},
|
||||
supportedOptions: [
|
||||
'Edit field title',
|
||||
'Display title',
|
||||
'Edit description',
|
||||
'Edit tooltip',
|
||||
'Operator',
|
||||
'Set validation rules',
|
||||
'Set default value',
|
||||
'Delete',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -8,7 +8,13 @@
|
||||
*/
|
||||
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import { BlockProvider, FixedBlockWrapper, SchemaComponentOptions, useBlockRequestContext } from '@nocobase/client';
|
||||
import {
|
||||
BlockProvider,
|
||||
FixedBlockWrapper,
|
||||
SchemaComponentOptions,
|
||||
useBlockRequestContext,
|
||||
useParsedFilter,
|
||||
} from '@nocobase/client';
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { theme } from 'antd';
|
||||
@ -54,11 +60,33 @@ const InternalMapBlockProvider = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const useMapBlockParams = (params: Record<string, any>) => {
|
||||
const { filter: parsedFilter, parseVariableLoading } = useParsedFilter({
|
||||
filterOption: params?.filter,
|
||||
});
|
||||
|
||||
return {
|
||||
params: {
|
||||
...params,
|
||||
filter: parsedFilter,
|
||||
} as Record<string, any>,
|
||||
parseVariableLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export const MapBlockProvider = (props) => {
|
||||
const uField = useField();
|
||||
const { params, fieldNames } = props;
|
||||
const { fieldNames } = props;
|
||||
const { params, parseVariableLoading } = useMapBlockParams(props.params);
|
||||
|
||||
// 在解析变量的时候不渲染,避免因为重复请求数据导致的资源浪费
|
||||
if (parseVariableLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const appends = params.appends || [];
|
||||
const { field } = fieldNames || {};
|
||||
|
||||
if (Array.isArray(field) && field.length > 1) {
|
||||
appends.push(field[0]);
|
||||
}
|
||||
|
@ -202,7 +202,7 @@ test('Collection event Add Data Trigger, determines that the trigger node single
|
||||
await page.getByRole('menuitemcheckbox', { name: triggerNodeFieldDisplayName }).click();
|
||||
const conditionalRightConstant = faker.lorem.words();
|
||||
await page.waitForTimeout(500);
|
||||
await page.keyboard.type(`=='${conditionalRightConstant}'`, { delay: 50 });
|
||||
await page.keyboard.type(`=='${conditionalRightConstant}'`, { delay: 100 });
|
||||
await expect(conditionNode.conditionExpressionEditBox).toHaveText(
|
||||
`Trigger variables / Trigger data / ${triggerNodeFieldDisplayName}=='${conditionalRightConstant}'`,
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user