mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
feat: support variables on the left side of linkage rule conditions (#6609)
* feat: support linkage rules setting for association block action * chore: linkage rule * fix: bug * fix: bug * refactor: linkage rule * refactor: code imporve * fix: bug * fix: bug * fix: bug * fix: bug * refactor: code imporve * fix: test * fix: test * fix: test * test: e2e test * fix: bug * fix: bug * fix: test * fix: e2e test * fix: bug * chore: support legacy leftVar in linkage rules * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: create submit linkage rule * fix: action panel * refactor: code improve * fix: bug * fix: test * fix: test * fix: code improve * fix: e2e test * fix: bug * refactor: refresh & expend action support linkage rule * refactor: code improve * fix: test * fix: bug * fix: bug * fix: e2e test * fix: e2e test * fix: e2e test * fix: test * refactor: code improve * fix: e2e test * fix: bug * fix: e2e test * fix: e2e test * fix: test * fix: e2e test * fix: e2e test * fix: bug
This commit is contained in:
parent
cb518dd162
commit
6fef3d7bed
@ -234,6 +234,10 @@ export default defineConfig({
|
||||
"title": "Filter",
|
||||
"link": "/components/filter"
|
||||
},
|
||||
{
|
||||
"title": "LinkageFilter",
|
||||
"link": "/components/linkage-filter"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -1582,7 +1582,7 @@ export const getAppends = ({
|
||||
const fieldNames = getTargetField(item);
|
||||
|
||||
// 只应该收集关系字段,只有大于 1 的时候才是关系字段
|
||||
if (fieldNames.length > 1) {
|
||||
if (fieldNames.length > 1 && !item.op) {
|
||||
appends.add(fieldNames.join('.'));
|
||||
}
|
||||
});
|
||||
|
@ -15,6 +15,8 @@ import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/actio
|
||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||
import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||
import { useDataBlockProps } from '../../../data-source';
|
||||
|
||||
export const addNewActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:addNew',
|
||||
@ -27,6 +29,16 @@ export const addNewActionSettings = new SchemaSettings({
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
SecondConFirm,
|
||||
RefreshDataBlockRequest,
|
||||
} from '../../../schema-component/antd/action/Action.Designer';
|
||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||
|
||||
export const bulkDeleteActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:bulkDelete',
|
||||
@ -27,6 +28,16 @@ export const bulkDeleteActionSettings = new SchemaSettings({
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'secondConFirm',
|
||||
Component: SecondConFirm,
|
||||
|
@ -32,11 +32,9 @@ export const disassociateActionSettings = new SchemaSettings({
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
collectionName: name,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -14,7 +14,7 @@ import { useDesignable } from '../../..';
|
||||
import { useSchemaToolbar } from '../../../application';
|
||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||
import { RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||
import { SchemaSettingsModalItem } from '../../../schema-settings';
|
||||
import { SchemaSettingsModalItem, SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||
|
||||
function ButtonEditor() {
|
||||
const field = useField();
|
||||
@ -110,6 +110,17 @@ export const expendableActionSettings = new SchemaSettings({
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'remove',
|
||||
sort: 100,
|
||||
|
@ -11,10 +11,9 @@ import { useField, useFieldSchema } from '@formily/react';
|
||||
import _ from 'lodash';
|
||||
import React, { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollectionRecord, useDesignable } from '../../../';
|
||||
import { useDesignable } 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,
|
||||
@ -22,6 +21,7 @@ import {
|
||||
SchemaSettingAccessControl,
|
||||
} from '../../../schema-settings';
|
||||
import { useURLAndHTMLSchema } from './useURLAndHTMLSchema';
|
||||
import { useDataBlockProps } from '../../../data-source';
|
||||
|
||||
export const SchemaSettingsActionLinkItem: FC = () => {
|
||||
const field = useField();
|
||||
@ -94,16 +94,10 @@ export const customizeLinkActionSettings = new SchemaSettings({
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useVisible() {
|
||||
const record = useCollectionRecord();
|
||||
return !_.isEmpty(record?.data);
|
||||
},
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
collectionName: name,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { useSchemaToolbar } from '../../../application';
|
||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||
import { ButtonEditor, RemoveButton, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer';
|
||||
|
||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||
export const refreshActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:refresh',
|
||||
items: [
|
||||
@ -22,6 +22,17 @@ export const refreshActionSettings = new SchemaSettings({
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'secondConFirm',
|
||||
Component: SecondConFirm,
|
||||
|
@ -29,6 +29,7 @@ import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks
|
||||
import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings';
|
||||
import { useParentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
|
||||
import { useDataBlockProps } from '../../../data-source';
|
||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||
|
||||
const Tree = connect(
|
||||
AntdTree,
|
||||
@ -149,6 +150,16 @@ export const createSubmitActionSettings = new SchemaSettings({
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'secondConfirmation',
|
||||
Component: SecondConFirm,
|
||||
|
@ -46,10 +46,6 @@ export const updateSubmitActionSettings = new SchemaSettings({
|
||||
collectionName: name,
|
||||
};
|
||||
},
|
||||
useVisible() {
|
||||
const fieldSchema = useFieldSchema();
|
||||
return !fieldSchema.parent['x-initializer'].includes('bulkEditForm');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'secondConfirmation',
|
||||
|
@ -6,17 +6,12 @@
|
||||
* 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 { useFieldSchema } from '@formily/react';
|
||||
import { useSchemaToolbar } from '../../../application';
|
||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||
import { useCollection_deprecated } from '../../../collection-manager';
|
||||
import { useCollection } from '../../../data-source';
|
||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||
import { SchemaSettingsLinkageRules, SchemaSettingAccessControl } from '../../../schema-settings';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
import { useCurrentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
|
||||
|
||||
export const customizePopupActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:popup',
|
||||
@ -33,18 +28,11 @@ export const customizePopupActionSettings = new SchemaSettings({
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
collectionName: name,
|
||||
};
|
||||
},
|
||||
useVisible() {
|
||||
const { collection } = useCurrentPopupRecord() || {};
|
||||
const currentCollection = useCollection();
|
||||
return !collection || collection?.name === currentCollection?.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'openMode',
|
||||
|
@ -35,11 +35,16 @@ test.describe('linkage rules', () => {
|
||||
// 条件:singleLineText 字段的值包含 123 时
|
||||
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByTestId('select-filter-field').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click();
|
||||
await page.getByLabel('Linkage rules').getByRole('tabpanel').getByRole('textbox').click();
|
||||
await page.getByLabel('Linkage rules').getByRole('tabpanel').getByRole('textbox').fill('123');
|
||||
|
||||
await page.getByLabel('variable-button').first().click();
|
||||
await page.getByText('Current form').last().click();
|
||||
await page.getByText('Current form').last().click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).locator('div').click();
|
||||
|
||||
// await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click();
|
||||
await page.getByTestId('right-filter-field').getByRole('textbox').click();
|
||||
await page.getByTestId('right-filter-field').getByRole('textbox').fill('123');
|
||||
await page.getByRole('tabpanel').getByRole('textbox').last().fill('123');
|
||||
// action:禁用 longText 字段
|
||||
await page.getByText('Add property').click();
|
||||
await page.getByTestId('select-linkage-property-field').click();
|
||||
@ -81,7 +86,7 @@ test.describe('linkage rules', () => {
|
||||
// 修改第一组规则,使其条件中包含一个变量 --------------------------------------------------------------------------
|
||||
// 当 singleLineText 字段的值包含 longText 字段的值时,禁用 longText 字段
|
||||
await openLinkageRules();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByLabel('variable-button').last().click();
|
||||
await expectSupportedVariables(page, [
|
||||
'Constant',
|
||||
'Current user',
|
||||
@ -136,8 +141,13 @@ test.describe('linkage rules', () => {
|
||||
.getByText('Add condition', { exact: true })
|
||||
.last()
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Select field' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'number' }).click();
|
||||
// await page.getByRole('button', { name: 'Select field' }).click();
|
||||
|
||||
await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click();
|
||||
await page.getByText('Current form').last().click();
|
||||
await page.getByText('Current form').last().click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'number' }).locator('div').click();
|
||||
|
||||
await page.getByLabel('Linkage rules').getByRole('spinbutton').click();
|
||||
await page.getByLabel('Linkage rules').getByRole('spinbutton').fill('123');
|
||||
|
||||
|
@ -22,7 +22,7 @@ test.describe('deprecated variables', () => {
|
||||
await expect(page.getByLabel('variable-tag').getByText('Current record / Nickname')).toBeVisible();
|
||||
|
||||
// 2. 但是变量列表中是禁用状态
|
||||
await page.locator('button').filter({ hasText: /^x$/ }).click();
|
||||
await page.locator('button').filter({ hasText: /^x$/ }).last().click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).hover({ position: { x: 40, y: 12 } });
|
||||
await expect(page.getByRole('tooltip', { name: 'This variable has been deprecated' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass(
|
||||
@ -45,11 +45,11 @@ test.describe('deprecated variables', () => {
|
||||
await page.getByLabel('Linkage rules').getByText('Linkage rules').click();
|
||||
|
||||
// 3. 当设置为其它变量后,再次打开,变量列表中的弃用变量不再显示
|
||||
await page.locator('button').filter({ hasText: /^x$/ }).click();
|
||||
await page.locator('button').filter({ hasText: /^x$/ }).last().click();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current form right' })).toHaveCount(1);
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname')).toBeVisible();
|
||||
await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname').last()).toBeVisible();
|
||||
// 清空表达式
|
||||
await page.getByLabel('textbox').clear();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
@ -58,7 +58,7 @@ test.describe('deprecated variables', () => {
|
||||
await page.getByLabel('block-item-CardItem-users-form').hover();
|
||||
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:editForm-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
||||
await page.locator('button').filter({ hasText: /^x$/ }).click();
|
||||
await page.locator('button').filter({ hasText: /^x$/ }).last().click();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toBeHidden();
|
||||
// 使下拉菜单消失
|
||||
await page.getByLabel('Linkage rules').getByText('Linkage rules').click();
|
||||
|
@ -86,7 +86,6 @@ test.describe('configure fields', () => {
|
||||
await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
|
||||
await page.mouse.move(600, 0);
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText(
|
||||
`manyToOne1:${record.manyToOne1.id}`,
|
||||
);
|
||||
|
@ -42,6 +42,7 @@ test.describe('where grid card block can be added', () => {
|
||||
await page.getByLabel('schema-initializer-Grid-').nth(1).hover();
|
||||
await page.getByRole('menuitem', { name: 'Role name' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await page.reload();
|
||||
await expect(page.getByText('Root')).toBeVisible();
|
||||
await expect(page.getByText('Admin')).toBeVisible();
|
||||
await expect(page.getByText('Member')).toBeVisible();
|
||||
|
@ -30,7 +30,6 @@ test('action linkage by row data', async ({ page, mockPage }) => {
|
||||
// 添加其他你需要的样式属性
|
||||
};
|
||||
});
|
||||
|
||||
expect(adminEditActionStyle.opacity).not.toBe('0.1');
|
||||
expect(rootEditActionStyle.opacity).not.toBe('1');
|
||||
});
|
||||
|
@ -316,7 +316,8 @@ test.describe('actions schema settings', () => {
|
||||
|
||||
// 添加一个条件:ID 等于 1
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByTestId('select-filter-field').click();
|
||||
await page.getByTestId('left-filter-field').getByLabel('variable-button').click();
|
||||
await page.getByText('Current record').last().click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||
await page.getByRole('spinbutton').click();
|
||||
await page.getByRole('spinbutton').fill('1');
|
||||
@ -340,7 +341,8 @@ test.describe('actions schema settings', () => {
|
||||
|
||||
// 添加一个条件:ID 等于 1
|
||||
await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).last().click();
|
||||
await page.getByRole('button', { name: 'Select field' }).click();
|
||||
await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click();
|
||||
await page.getByText('Current record').last().click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||
await page.getByRole('spinbutton').click();
|
||||
await page.getByRole('spinbutton').fill('1');
|
||||
@ -902,7 +904,6 @@ test.describe('actions schema settings', () => {
|
||||
await page.getByRole('menuitem', { name: 'Submit' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await page.getByLabel('designer-schema-settings-CardItem-TableBlockDesigner-treeCollection').hover();
|
||||
await page.getByRole('menuitem', { name: 'Tree table' }).click();
|
||||
|
||||
@ -928,6 +929,7 @@ test.describe('actions schema settings', () => {
|
||||
await page.getByLabel('schema-initializer-Grid-form:').hover();
|
||||
await page.getByRole('menuitem', { name: 'Parent', exact: true }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await page.reload();
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('block-item-CollectionField-')
|
||||
|
@ -27,7 +27,8 @@ test.describe('options of Select field in linkage rule', () => {
|
||||
await page.getByRole('switch', { name: 'On Off' }).click();
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
await page.reload();
|
||||
await expect(page.getByRole('option', { name: 'option2' })).toBeVisible();
|
||||
await page.getByLabel('block-item-CollectionField-').click();
|
||||
await expect(page.getByRole('option', { name: 'option2' }).last()).toBeVisible();
|
||||
await expect(page.getByRole('option', { name: 'option3' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -215,7 +215,7 @@ test.describe('where to open a popup and what can be added to it', () => {
|
||||
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
|
||||
|
||||
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'Table right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Table right' }).click();
|
||||
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
|
||||
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'One to many' }).click();
|
||||
@ -282,7 +282,7 @@ test.describe('where to open a popup and what can be added to it', () => {
|
||||
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
|
||||
|
||||
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'Table right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Table right' }).click();
|
||||
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
|
||||
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'One to many' }).click();
|
||||
|
@ -18,7 +18,7 @@ test.describe('variables', () => {
|
||||
await page.getByLabel('action-Action.Link-View-view-').hover();
|
||||
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByTestId('left-filter-field').getByLabel('variable-button').click();
|
||||
|
||||
// 2. 断言应该显示的变量
|
||||
['Constant', 'Current user', 'Current role', 'API token', 'Date variables', 'Current record'].forEach(
|
||||
|
@ -22,14 +22,14 @@ test.describe('variable: Current Record', () => {
|
||||
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
||||
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByLabel('variable-button').first().click();
|
||||
|
||||
// 当前表单中应该包含 “Nickname” 字段
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
|
||||
// 当前对象中应该包含 “Role UID” 字段
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByLabel('variable-button').first().click();
|
||||
await page.getByText('Current object').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Role UID' }).click();
|
||||
@ -43,12 +43,12 @@ test.describe('variable: Current Record', () => {
|
||||
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
||||
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByLabel('variable-button').first().click();
|
||||
|
||||
// 当前记录中应该包含 “Nickname” 字段
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByLabel('variable-button').first().click();
|
||||
|
||||
// 当前对象中应该包含 “Role UID” 字段
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();
|
||||
|
@ -49,6 +49,7 @@ import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
|
||||
import { ActionContextProps, ActionProps, ComposedAction } from './types';
|
||||
import { linkageAction, setInitialActionState } from './utils';
|
||||
import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant';
|
||||
import { BlockContext } from '../../../block-provider/BlockProvider';
|
||||
|
||||
// 这个要放到最下面,否则会导致前端单测失败
|
||||
import { useApp } from '../../../application';
|
||||
@ -96,7 +97,9 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
||||
const { designable } = useDesignable();
|
||||
const tarComponent = useComponent(component) || component;
|
||||
const variables = useVariables();
|
||||
const localVariables = useLocalVariables({ currentForm: { values: recordData, readPretty: false } as any });
|
||||
const localVariables = useLocalVariables({
|
||||
currentForm: { values: recordData, readPretty: false } as any,
|
||||
});
|
||||
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
|
||||
const { setSubmitted } = useActionContext();
|
||||
const { getAriaLabel } = useGetAriaLabelOfAction(title);
|
||||
@ -120,6 +123,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
||||
condition: v.condition,
|
||||
variables,
|
||||
localVariables,
|
||||
conditionType: v.conditionType,
|
||||
},
|
||||
app.jsonLogic,
|
||||
);
|
||||
@ -155,36 +159,38 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
||||
}, [onClick, fieldSchema, getAllDataBlocks]);
|
||||
|
||||
return (
|
||||
<InternalAction
|
||||
containerRefKey={containerRefKey}
|
||||
fieldSchema={fieldSchema}
|
||||
designable={designable}
|
||||
field={field}
|
||||
icon={icon}
|
||||
loading={loading}
|
||||
handleMouseEnter={handleMouseEnter}
|
||||
tarComponent={tarComponent}
|
||||
className={className}
|
||||
type={props.type}
|
||||
Designer={Designer}
|
||||
onClick={handleClick}
|
||||
confirm={confirm}
|
||||
confirmTitle={confirmTitle}
|
||||
popover={popover}
|
||||
addChild={addChild}
|
||||
recordData={recordData}
|
||||
title={title}
|
||||
style={style}
|
||||
propsDisabled={propsDisabled}
|
||||
useAction={useAction}
|
||||
visibleWithURL={visibleWithURL}
|
||||
setVisibleWithURL={setVisibleWithURL}
|
||||
setSubmitted={setSubmitted}
|
||||
getAriaLabel={getAriaLabel}
|
||||
parentRecordData={parentRecordData}
|
||||
actionCallback={actionCallback}
|
||||
{...others}
|
||||
/>
|
||||
<BlockContext.Provider value={{ name: 'action' }}>
|
||||
<InternalAction
|
||||
containerRefKey={containerRefKey}
|
||||
fieldSchema={fieldSchema}
|
||||
designable={designable}
|
||||
field={field}
|
||||
icon={icon}
|
||||
loading={loading}
|
||||
handleMouseEnter={handleMouseEnter}
|
||||
tarComponent={tarComponent}
|
||||
className={className}
|
||||
type={props.type}
|
||||
Designer={Designer}
|
||||
onClick={onClick}
|
||||
confirm={confirm}
|
||||
confirmTitle={confirmTitle}
|
||||
popover={popover}
|
||||
addChild={addChild}
|
||||
recordData={recordData}
|
||||
title={title}
|
||||
style={style}
|
||||
propsDisabled={propsDisabled}
|
||||
useAction={useAction}
|
||||
visibleWithURL={visibleWithURL}
|
||||
setVisibleWithURL={setVisibleWithURL}
|
||||
setSubmitted={setSubmitted}
|
||||
getAriaLabel={getAriaLabel}
|
||||
parentRecordData={parentRecordData}
|
||||
actionCallback={actionCallback}
|
||||
{...others}
|
||||
/>
|
||||
</BlockContext.Provider>
|
||||
);
|
||||
}),
|
||||
{ displayName: 'Action' },
|
||||
|
@ -32,7 +32,7 @@ export const useGetAriaLabelOfAction = (title: string) => {
|
||||
let { name: blockName } = useBlockContext() || {};
|
||||
const actionTitle = title || compile(fieldSchema.title);
|
||||
collectionName = collectionName ? `-${collectionName}` : '';
|
||||
blockName = blockName ? `-${blockName}` : '';
|
||||
blockName = blockName && blockName !== 'action' ? `-${blockName}` : '';
|
||||
action = action ? `-${action}` : '';
|
||||
recordName = recordName ? `-${recordName}` : '';
|
||||
|
||||
|
@ -87,12 +87,14 @@ export const linkageAction = async (
|
||||
condition,
|
||||
variables,
|
||||
localVariables,
|
||||
conditionType,
|
||||
}: {
|
||||
operator;
|
||||
field;
|
||||
condition;
|
||||
variables: VariablesContextType;
|
||||
localVariables: VariableOption[];
|
||||
conditionType: 'advanced' | 'basic';
|
||||
},
|
||||
jsonLogic: any,
|
||||
) => {
|
||||
@ -101,7 +103,7 @@ export const linkageAction = async (
|
||||
|
||||
switch (operator) {
|
||||
case ActionType.Visible:
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
|
||||
displayResult.push(operator);
|
||||
field.data = field.data || {};
|
||||
field.data.hidden = false;
|
||||
@ -113,7 +115,7 @@ export const linkageAction = async (
|
||||
field.display = last(displayResult);
|
||||
break;
|
||||
case ActionType.Hidden:
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
|
||||
field.data = field.data || {};
|
||||
field.data.hidden = true;
|
||||
} else {
|
||||
@ -122,7 +124,7 @@ export const linkageAction = async (
|
||||
}
|
||||
break;
|
||||
case ActionType.Disabled:
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
|
||||
disableResult.push(true);
|
||||
}
|
||||
field.stateOfLinkageRules = {
|
||||
@ -133,7 +135,7 @@ export const linkageAction = async (
|
||||
field.componentProps['disabled'] = last(disableResult);
|
||||
break;
|
||||
case ActionType.Active:
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
|
||||
disableResult.push(false);
|
||||
} else {
|
||||
disableResult.push(!!field.componentProps?.['disabled']);
|
||||
|
@ -27,7 +27,7 @@ export const useGetAriaLabelOfBlockItem = (name?: string) => {
|
||||
let { name: blockName } = useBlockContext() || {};
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { name: collectionName, getField } = useCollection_deprecated();
|
||||
blockName = name || blockName;
|
||||
blockName = name || (blockName !== 'action' ? blockName : '');
|
||||
|
||||
const title = compile(fieldSchema['title']) || compile(getField(fieldSchema.name)?.uiSchema?.title);
|
||||
|
||||
|
@ -54,7 +54,7 @@ describe('CollectionSelect', () => {
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="css-a7w9kk ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-1rquknz"
|
||||
class="css-9mlexe ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-1rquknz"
|
||||
>
|
||||
<div
|
||||
class="ant-formily-item-label"
|
||||
@ -195,7 +195,7 @@ describe('CollectionSelect', () => {
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="css-a7w9kk ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-1rquknz"
|
||||
class="css-9mlexe ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-1rquknz"
|
||||
>
|
||||
<div
|
||||
class="ant-formily-item-label"
|
||||
|
@ -39,15 +39,13 @@ const formItemWrapCss = css`
|
||||
.ant-description-textarea img {
|
||||
max-width: 100%;
|
||||
}
|
||||
&.ant-formily-item-layout-horizontal.ant-formily-item-label-wrap {
|
||||
.ant-formily-item-label {
|
||||
&.ant-formily-item-layout-vertical .ant-formily-item-label {
|
||||
display: inline;
|
||||
.ant-formily-item-label-tooltip-icon {
|
||||
display: inline;
|
||||
}
|
||||
.ant-formily-item-label-content {
|
||||
display: inline;
|
||||
padding-right: 5px;
|
||||
|
||||
.ant-formily-item-label-tooltip-icon,
|
||||
.ant-formily-item-label-content {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -66,4 +66,5 @@ export * from './unix-timestamp';
|
||||
export * from './upload';
|
||||
export * from './variable';
|
||||
export * from './form-drawer';
|
||||
export * from './linkageFilter';
|
||||
import './index.less';
|
||||
|
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 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 { createForm, onFieldValueChange } from '@formily/core';
|
||||
import { FieldContext, FormContext } from '@formily/react';
|
||||
import { merge } from '@formily/shared';
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
|
||||
import { SchemaComponent } from '../../core';
|
||||
import { FilterContext } from './context';
|
||||
import { VariableInput, getShouldChange } from '../../../schema-settings/VariableInput/VariableInput';
|
||||
import { useCollectionRecordData } from '../../../data-source';
|
||||
import { useLocalVariables, useVariables } from '../../../variables';
|
||||
import { useCollectionManager_deprecated } from '../../../collection-manager';
|
||||
|
||||
export interface DynamicComponentProps {
|
||||
value: any;
|
||||
/**
|
||||
* `Filter` 组件左侧选择的字段
|
||||
*/
|
||||
collectionField: CollectionFieldOptions_deprecated;
|
||||
onChange: (value: any) => void;
|
||||
renderSchemaComponent: () => React.JSX.Element;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value: any;
|
||||
collectionField?: CollectionFieldOptions_deprecated;
|
||||
onChange: (value: any) => void;
|
||||
style?: React.CSSProperties;
|
||||
componentProps?: any;
|
||||
schema?: any;
|
||||
setScopes?: any;
|
||||
testid?: string;
|
||||
nullable?: boolean;
|
||||
constantAbel?: boolean;
|
||||
changeOnSelect?: boolean;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export const DynamicComponent = (props: Props) => {
|
||||
const { setScopes, nullable, constantAbel, changeOnSelect, readOnly = false } = props;
|
||||
const { disabled } = useContext(FilterContext) || {};
|
||||
const record = useCollectionRecordData();
|
||||
const variables = useVariables();
|
||||
const localVariables = useLocalVariables();
|
||||
const { getAllCollectionsInheritChain } = useCollectionManager_deprecated();
|
||||
const { collectionField } = props;
|
||||
const component = useCallback((props: DynamicComponentProps) => {
|
||||
return (
|
||||
<VariableInput
|
||||
{...props}
|
||||
form={form}
|
||||
record={record}
|
||||
setScopes={setScopes}
|
||||
nullable={nullable}
|
||||
constantAbel={constantAbel}
|
||||
changeOnSelect={changeOnSelect}
|
||||
shouldChange={getShouldChange({
|
||||
collectionField,
|
||||
variables,
|
||||
localVariables,
|
||||
getAllCollectionsInheritChain,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
const form = useMemo(() => {
|
||||
return createForm({
|
||||
values: {
|
||||
value: props.value,
|
||||
},
|
||||
effects() {
|
||||
onFieldValueChange('value', (field) => {
|
||||
props?.onChange?.(field.value);
|
||||
});
|
||||
},
|
||||
disabled,
|
||||
});
|
||||
}, [JSON.stringify(props.value), props.schema]);
|
||||
const renderSchemaComponent: any = useCallback(() => {
|
||||
const componentProps = merge(props?.schema?.['x-component-props'] || {}, props.componentProps || {});
|
||||
|
||||
return (
|
||||
<FieldContext.Provider value={null}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
'x-component': 'Input',
|
||||
...props.schema,
|
||||
'x-component-props': merge(componentProps, {
|
||||
style: {
|
||||
minWidth: 150,
|
||||
...props.style,
|
||||
},
|
||||
utc: false,
|
||||
readOnly: readOnly,
|
||||
}),
|
||||
name: 'value',
|
||||
'x-read-pretty': false,
|
||||
'x-validator': undefined,
|
||||
'x-decorator': undefined,
|
||||
}}
|
||||
/>
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
}, [props.schema]);
|
||||
return (
|
||||
<FormContext.Provider value={form}>
|
||||
<div data-testid={props.testid}>
|
||||
{React.createElement<DynamicComponentProps>(component, {
|
||||
value: props.value,
|
||||
collectionField: props.collectionField,
|
||||
onChange: props?.onChange,
|
||||
renderSchemaComponent,
|
||||
})}
|
||||
</div>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const FilterDynamicComponent = DynamicComponent;
|
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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 { CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { ObjectField as ObjectFieldModel } from '@formily/core';
|
||||
import { ArrayField, connect, useField } from '@formily/react';
|
||||
import { Select, Space } from 'antd';
|
||||
import React, { useContext } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useToken } from '../__builtins__';
|
||||
import { FilterItems } from './FilterItems';
|
||||
import { FilterLogicContext, RemoveConditionContext } from './context';
|
||||
|
||||
export const FilterGroup = connect((props) => {
|
||||
const { bordered = true, disabled } = props;
|
||||
const field = useField<ObjectFieldModel>();
|
||||
const remove = useContext(RemoveConditionContext);
|
||||
const { t } = useTranslation();
|
||||
const { token } = useToken();
|
||||
|
||||
const keys = Object.keys(field.value || {});
|
||||
const logic = keys.includes('$or') ? '$or' : '$and';
|
||||
const setLogic = (value) => {
|
||||
const obj = field.value || {};
|
||||
field.value = {
|
||||
[value]: [...(obj[logic] || [])],
|
||||
};
|
||||
};
|
||||
const mergedDisabled = disabled || field.disabled;
|
||||
return (
|
||||
<FilterLogicContext.Provider value={logic}>
|
||||
<div
|
||||
style={
|
||||
bordered
|
||||
? {
|
||||
position: 'relative',
|
||||
border: `1px dashed ${token.colorBorder}`,
|
||||
padding: token.paddingSM,
|
||||
marginBottom: token.marginXS,
|
||||
}
|
||||
: {
|
||||
position: 'relative',
|
||||
marginBottom: token.marginXS,
|
||||
}
|
||||
}
|
||||
>
|
||||
{remove && !mergedDisabled && (
|
||||
<a role="button" aria-label="icon-close">
|
||||
<CloseCircleOutlined
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
top: 10,
|
||||
color: '#bfbfbf',
|
||||
}}
|
||||
onClick={() => remove()}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
<div style={{ marginBottom: 8, color: token.colorText }}>
|
||||
<Trans>
|
||||
{'Meet '}
|
||||
<Select
|
||||
// @ts-ignore
|
||||
role="button"
|
||||
data-testid="filter-select-all-or-any"
|
||||
style={{ width: 'auto' }}
|
||||
value={logic}
|
||||
onChange={(value) => {
|
||||
setLogic(value);
|
||||
}}
|
||||
>
|
||||
<Select.Option value={'$and'}>All</Select.Option>
|
||||
<Select.Option value={'$or'}>Any</Select.Option>
|
||||
</Select>
|
||||
{' conditions in the group'}
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<ArrayField name={`${logic}`} component={[FilterItems]} disabled={mergedDisabled} />
|
||||
</div>
|
||||
{!mergedDisabled && (
|
||||
<Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
|
||||
<a
|
||||
onClick={() => {
|
||||
const value = field.value || {};
|
||||
const items = value[logic] || [];
|
||||
items.push({});
|
||||
field.value = {
|
||||
[logic]: items,
|
||||
};
|
||||
field.initialValue = {
|
||||
[logic]: items,
|
||||
};
|
||||
}}
|
||||
>
|
||||
{t('Add condition')}
|
||||
</a>
|
||||
<a
|
||||
onClick={() => {
|
||||
const value = field.value || {};
|
||||
const items = value[logic] || [];
|
||||
items.push({
|
||||
$and: [{}],
|
||||
});
|
||||
field.value = {
|
||||
[logic]: items,
|
||||
};
|
||||
}}
|
||||
>
|
||||
{t('Add condition group')}
|
||||
</a>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</FilterLogicContext.Provider>
|
||||
);
|
||||
});
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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 { ArrayField as ArrayFieldModel } from '@formily/core';
|
||||
import { ObjectField, observer, useField } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { FilterGroup } from './FilterGroup';
|
||||
import { LinkageFilterItem } from './LinkageFilterItem';
|
||||
import { RemoveConditionContext } from './context';
|
||||
|
||||
export const FilterItems = observer(
|
||||
(props) => {
|
||||
const field = useField<ArrayFieldModel>();
|
||||
return (
|
||||
<div>
|
||||
{field?.value?.filter(Boolean).map((item, index) => {
|
||||
return (
|
||||
<RemoveConditionContext.Provider key={index} value={() => field.remove(index)}>
|
||||
<ObjectField name={index} component={[item.$and || item.$or ? FilterGroup : LinkageFilterItem]} />
|
||||
</RemoveConditionContext.Provider>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ displayName: 'FilterItems' },
|
||||
);
|
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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 { ObjectField as ObjectFieldModel } from '@formily/core';
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { UseRequestOptions, useRequest } from '../../../api-client';
|
||||
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import { FilterGroup } from './FilterGroup';
|
||||
import { FilterContext } from './context';
|
||||
|
||||
const useDef = (options: UseRequestOptions) => {
|
||||
const field = useField<ObjectFieldModel>();
|
||||
return useRequest(() => Promise.resolve({ data: field.dataSource }), options);
|
||||
};
|
||||
|
||||
export const LinkageFilter: any = withDynamicSchemaProps(
|
||||
observer((props: any) => {
|
||||
const { useDataSource = useDef } = props;
|
||||
|
||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const { dynamicComponent, className, collectionName } = useProps(props);
|
||||
const [scopes, setScopes] = useState([]);
|
||||
|
||||
const field = useField<ObjectFieldModel>();
|
||||
const fieldSchema: any = useFieldSchema();
|
||||
useDataSource({
|
||||
onSuccess(data) {
|
||||
field.dataSource = data?.data || [];
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (fieldSchema.defaultValue) {
|
||||
field.initialValue = fieldSchema.defaultValue;
|
||||
}
|
||||
}, [fieldSchema.defaultValue]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<FilterContext.Provider
|
||||
value={{
|
||||
field,
|
||||
fieldSchema,
|
||||
dynamicComponent,
|
||||
disabled: props.disabled,
|
||||
collectionName,
|
||||
scopes,
|
||||
setScopes,
|
||||
}}
|
||||
>
|
||||
<FilterGroup {...props} bordered={false} />
|
||||
</FilterContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
{ displayName: 'LinkageFilter' },
|
||||
);
|
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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 { CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { observer } from '@formily/react';
|
||||
import { Select, Space } from 'antd';
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { DynamicComponent } from './DynamicComponent';
|
||||
import { RemoveConditionContext } from './context';
|
||||
import { useValues } from './useValues';
|
||||
import { FilterContext } from './context';
|
||||
|
||||
export const LinkageFilterItem = observer(
|
||||
(props: any) => {
|
||||
const { t } = useTranslation();
|
||||
const compile = useCompile();
|
||||
const remove = useContext(RemoveConditionContext);
|
||||
const { setScopes } = useContext(FilterContext) || {};
|
||||
const { schema, operators, operator, setOperator, rightVar, leftVar, setLeftValue, setRightValue } = useValues();
|
||||
const style = useMemo(() => ({ marginBottom: 8 }), []);
|
||||
|
||||
const onOperatorsChange = useCallback(
|
||||
(value) => {
|
||||
setOperator(value);
|
||||
},
|
||||
[setOperator],
|
||||
);
|
||||
const removeStyle = useMemo(() => ({ color: '#bfbfbf' }), []);
|
||||
return (
|
||||
// 添加 nc-filter-item 类名是为了帮助编写测试时更容易选中该元素
|
||||
<div style={style} className="nc-filter-item">
|
||||
<Space wrap>
|
||||
<DynamicComponent
|
||||
value={leftVar}
|
||||
onChange={setLeftValue}
|
||||
setScopes={setScopes}
|
||||
testid="left-filter-field"
|
||||
nullable={false}
|
||||
constantAbel={false}
|
||||
changeOnSelect={false}
|
||||
readOnly={true}
|
||||
/>
|
||||
<Select
|
||||
// @ts-ignore
|
||||
role="button"
|
||||
data-testid="select-filter-operator"
|
||||
className={css`
|
||||
min-width: 110px;
|
||||
`}
|
||||
popupMatchSelectWidth={false}
|
||||
value={operator?.value}
|
||||
options={compile(operators)}
|
||||
onChange={onOperatorsChange}
|
||||
placeholder={t('Comparision')}
|
||||
/>
|
||||
{!operator?.noValue ? (
|
||||
<DynamicComponent value={rightVar} schema={schema} onChange={setRightValue} testid="right-filter-field" />
|
||||
) : null}
|
||||
{!props.disabled && (
|
||||
<a role="button" aria-label="icon-close">
|
||||
<CloseCircleOutlined onClick={remove} style={removeStyle} />
|
||||
</a>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ displayName: 'FilterItem' },
|
||||
);
|
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 { ObjectField } from '@formily/core';
|
||||
import { Schema } from '@formily/react';
|
||||
import { ComponentType, createContext } from 'react';
|
||||
import { DynamicComponentProps } from './DynamicComponent';
|
||||
|
||||
export interface FilterContextProps {
|
||||
field?: ObjectField & { collectionName?: string };
|
||||
fieldSchema?: Schema;
|
||||
dynamicComponent?: ComponentType<DynamicComponentProps>;
|
||||
disabled?: boolean;
|
||||
collectionName?: string;
|
||||
scopes?: any[];
|
||||
setScopes?: any;
|
||||
}
|
||||
|
||||
export const RemoveConditionContext = createContext(null);
|
||||
RemoveConditionContext.displayName = 'RemoveConditionContext';
|
||||
export const FilterContext = createContext<FilterContextProps>(null);
|
||||
FilterContext.displayName = 'FilterContext';
|
||||
export const FilterLogicContext = createContext(null);
|
||||
FilterLogicContext.displayName = 'FilterLogicContext';
|
@ -0,0 +1,148 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useField, observer, ISchema } from '@formily/react';
|
||||
import { FilterActionProps, useRequest, SchemaComponent, Plugin } from '@nocobase/client';
|
||||
import { ArrayCollapse, FormLayout } from '@formily/antd-v5';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { mockApp } from '@nocobase/client/demo-utils';
|
||||
|
||||
const ShowFilterData = observer(({ children }) => {
|
||||
const field = useField<any>();
|
||||
return (
|
||||
<>
|
||||
<pre>{JSON.stringify(field.value, null, 2)}</pre>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const useFilterActionProps = (): FilterActionProps => {
|
||||
const field = useField<any>();
|
||||
const { run } = useRequest({ url: 'test' }, { manual: true });
|
||||
|
||||
return {
|
||||
onSubmit: async (values) => {
|
||||
console.log('onSubmit', values);
|
||||
|
||||
// request api
|
||||
run(values);
|
||||
|
||||
field.setValue(values);
|
||||
},
|
||||
onReset: (values) => {
|
||||
console.log('onReset', values);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
'x-decorator': 'FormV2',
|
||||
'x-component': 'ShowFormData',
|
||||
properties: {
|
||||
rules: {
|
||||
type: 'array',
|
||||
default: [{}],
|
||||
'x-component': 'ArrayCollapse',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': {
|
||||
accordion: true,
|
||||
},
|
||||
items: {
|
||||
type: 'object',
|
||||
'x-component': 'ArrayCollapse.CollapsePanel',
|
||||
'x-component-props': {
|
||||
// extra: 'linkage rule',
|
||||
},
|
||||
properties: {
|
||||
layout: {
|
||||
type: 'void',
|
||||
'x-component': 'FormLayout',
|
||||
'x-component-props': {
|
||||
labelStyle: {
|
||||
marginTop: '6px',
|
||||
},
|
||||
labelCol: 8,
|
||||
wrapperCol: 16,
|
||||
},
|
||||
properties: {
|
||||
conditions: {
|
||||
'x-component': 'h4',
|
||||
'x-content': '{{ t("Condition") }}',
|
||||
},
|
||||
condition: {
|
||||
'x-component': 'LinkageFilter',
|
||||
'x-use-component-props': () => {
|
||||
return {
|
||||
// options,
|
||||
className: css`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-left: 10px;
|
||||
`,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.Remove',
|
||||
},
|
||||
moveUp: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.MoveUp',
|
||||
},
|
||||
moveDown: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.MoveDown',
|
||||
},
|
||||
copy: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.Copy',
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
add: {
|
||||
type: 'void',
|
||||
title: '{{ t("Add linkage rule") }}',
|
||||
'x-component': 'ArrayCollapse.Addition',
|
||||
'x-reactions': {
|
||||
dependencies: ['rules'],
|
||||
fulfill: {
|
||||
state: {
|
||||
// disabled: '{{$deps[0].length >= 3}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
components={{ ShowFilterData, ArrayCollapse, FormLayout }}
|
||||
scope={{ useFilterActionProps }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
class DemoPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.router.add('root', { path: '/', Component: Demo });
|
||||
}
|
||||
}
|
||||
|
||||
const app = mockApp({
|
||||
plugins: [DemoPlugin],
|
||||
apis: {
|
||||
test: { data: { data: 'ok' } },
|
||||
},
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import { useField, observer } from '@formily/react';
|
||||
import { FilterActionProps, ISchema, useDataBlockRequest } from '@nocobase/client';
|
||||
import { mockApp } from '@nocobase/client/demo-utils';
|
||||
import { SchemaComponent, Plugin } from '@nocobase/client';
|
||||
|
||||
const ShowFilterData = observer(({ children }) => {
|
||||
const field = useField<any>();
|
||||
return (
|
||||
<>
|
||||
<pre>{JSON.stringify(field.value, null, 2)}</pre>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const useFilterActionProps = (): FilterActionProps => {
|
||||
const field = useField<any>();
|
||||
const { run } = useDataBlockRequest(); // replace `useRequest`
|
||||
|
||||
return {
|
||||
onSubmit: async (values) => {
|
||||
console.log('onSubmit', values);
|
||||
|
||||
// request api
|
||||
run(values);
|
||||
|
||||
field.setValue(values);
|
||||
},
|
||||
onReset: (values) => {
|
||||
console.log('onReset', values);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
name: 'root',
|
||||
'x-decorator': 'DataBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
action: 'list',
|
||||
},
|
||||
properties: {
|
||||
test: {
|
||||
name: 'filter',
|
||||
type: 'object',
|
||||
title: 'Filter',
|
||||
'x-decorator': 'ShowFilterData',
|
||||
'x-component': 'Filter.Action',
|
||||
'x-use-component-props': 'useFilterActionProps',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
components={{ ShowFilterData }}
|
||||
scope={{
|
||||
useFilterActionProps,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
class DemoPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.router.add('root', { path: '/', Component: Demo });
|
||||
}
|
||||
}
|
||||
|
||||
const app = mockApp({
|
||||
plugins: [DemoPlugin],
|
||||
apis: {
|
||||
test: { data: { data: 'ok' } },
|
||||
},
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
@ -0,0 +1,17 @@
|
||||
# LinkageFilter
|
||||
|
||||
A component used for filtering data, commonly used to filter data in blocks.
|
||||
|
||||
```ts
|
||||
type FilterActionProps<T = {}> = ActionProps & {
|
||||
options?: any[];
|
||||
form?: Form;
|
||||
onSubmit?: (values: T) => void;
|
||||
onReset?: (values: T) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
|
||||
<code src="./demos/new-demos/basic.tsx"></code>
|
@ -0,0 +1,18 @@
|
||||
# LinkageFilter
|
||||
|
||||
用于前端联动规则中,用作条件配置
|
||||
|
||||
```ts
|
||||
type FilterActionProps<T = {}> = ActionProps & {
|
||||
options?: any[];
|
||||
form?: Form;
|
||||
onSubmit?: (values: T) => void;
|
||||
onReset?: (values: T) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
左侧支持变量,操作符、和右侧变量组件跟随左侧变量联动
|
||||
|
||||
<code src="./demos/new-demos/basic.tsx"></code>
|
||||
|
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 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 * from './LinkageFilter';
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { useFieldSchema } from '@formily/react';
|
||||
import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* 获取当前字段所支持的操作符列表
|
||||
* @returns
|
||||
*/
|
||||
export const useOperatorList = (): any[] => {
|
||||
const schema = useFieldSchema();
|
||||
const { name } = useCollection_deprecated();
|
||||
const { getCollectionFields, getInterface } = useCollectionManager_deprecated();
|
||||
|
||||
const res = useMemo(() => {
|
||||
const fieldInterface = schema['x-designer-props']?.interface;
|
||||
const collectionFields = getCollectionFields(name);
|
||||
if (fieldInterface) {
|
||||
return getInterface(fieldInterface)?.filterable?.operators || [];
|
||||
}
|
||||
const field = collectionFields.find((item) => item.name === schema.name);
|
||||
const ops = getInterface(field?.interface)?.filterable?.operators || [];
|
||||
return ops.filter((o) => typeof o.visible !== 'function' || o.visible(field));
|
||||
}, [schema.name]);
|
||||
return res;
|
||||
};
|
@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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 { useField } from '@formily/react';
|
||||
import { merge } from '@formily/shared';
|
||||
import { cloneDeep, last, uniqBy } from 'lodash';
|
||||
import { useCallback, useContext, useEffect } from 'react';
|
||||
import { FilterContext } from './context';
|
||||
|
||||
interface UseValuesReturn {
|
||||
fields: any[];
|
||||
collectionField: any;
|
||||
dataIndex: string[];
|
||||
operators: any[];
|
||||
operator: any;
|
||||
schema: any;
|
||||
value: any;
|
||||
setDataIndex: (dataIndex: string[]) => void;
|
||||
setOperator: (operatorValue: string) => void;
|
||||
setRightValue: (value: any) => void;
|
||||
setLeftValue: (value: any) => void;
|
||||
leftVar: any;
|
||||
rightVar: any;
|
||||
}
|
||||
|
||||
const findOption = (str, options) => {
|
||||
if (!str) return null;
|
||||
const match = str.match(/\{\{\$(.*?)\}\}/);
|
||||
if (!match) return null;
|
||||
|
||||
const [firstKey, ...subKeys] = match[1].split('.'); // 拆分层级
|
||||
const keys = [`$${firstKey}`, ...subKeys]; // 第一层保留 `$`,后续不带 `$`
|
||||
|
||||
let currentOptions = options;
|
||||
let option = null;
|
||||
for (const key of keys) {
|
||||
option = currentOptions.find((opt) => opt.value === key);
|
||||
if (!option) return null;
|
||||
|
||||
// 进入下一层 children 查找
|
||||
if (Array.isArray(option.children) || option.isLeaf === false) {
|
||||
currentOptions = option.children;
|
||||
} else {
|
||||
return option; // 没有 children 直接返回
|
||||
}
|
||||
}
|
||||
|
||||
return option;
|
||||
};
|
||||
const operators = [
|
||||
{ label: '{{t("is empty")}}', value: '$empty', noValue: true },
|
||||
{ label: '{{t("is not empty")}}', value: '$notEmpty', noValue: true },
|
||||
];
|
||||
export const useValues = (): UseValuesReturn => {
|
||||
const field = useField<any>();
|
||||
const { scopes } = useContext(FilterContext) || {};
|
||||
const { op, leftVar, rightVar } = field.value || {};
|
||||
const data2value = useCallback(() => {
|
||||
field.value = field.data.leftVar
|
||||
? {
|
||||
op: field.data.operator?.value,
|
||||
leftVar: field.data.leftVar,
|
||||
rightVar: field.data?.rightVar,
|
||||
}
|
||||
: {};
|
||||
}, [field]);
|
||||
|
||||
const value2data = () => {
|
||||
/**
|
||||
* 等待获取最新的scopes
|
||||
*/
|
||||
setTimeout(() => {
|
||||
const option = findOption(leftVar, scopes);
|
||||
field.data = field.data || {};
|
||||
if (!field.value) {
|
||||
return;
|
||||
}
|
||||
const combOperators = uniqBy([...(field.data.operators || []), ...(option?.operators || [])], 'value');
|
||||
field.data.operators = combOperators.length ? combOperators : operators;
|
||||
field.data.leftVar = leftVar;
|
||||
field.data.rightVar = rightVar;
|
||||
const operator = combOperators?.find((v) => v.value === op);
|
||||
field.data.operator = field.data.operator || operator;
|
||||
const s1 = cloneDeep(option?.schema);
|
||||
const s2 = cloneDeep(operator?.schema);
|
||||
field.data.schema = field.data?.schema || merge(s1, s2);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
useEffect(value2data, [field.value, scopes]);
|
||||
|
||||
const setLeftValue = useCallback(
|
||||
(leftVar, paths) => {
|
||||
const option: any = last(paths);
|
||||
field.data = field.data || {};
|
||||
field.data.operators = option?.operators || operators;
|
||||
const operator = field.data.operators?.[0];
|
||||
field.data.operator = operator;
|
||||
const s1 = cloneDeep(option?.schema);
|
||||
const s2 = cloneDeep(operator?.schema);
|
||||
field.data.schema = merge(s1, s2);
|
||||
field.data.leftVar = leftVar;
|
||||
field.data.rightVar = operator?.noValue ? operator.default || true : undefined;
|
||||
data2value();
|
||||
},
|
||||
[data2value, field],
|
||||
);
|
||||
|
||||
const setOperator = useCallback(
|
||||
(operatorValue) => {
|
||||
const operator = field.data?.operators?.find?.((item) => item.value === operatorValue);
|
||||
field.data.operator = operator;
|
||||
const s1 = cloneDeep(field.data.schema);
|
||||
const s2 = cloneDeep(operator?.schema);
|
||||
field.data.schema = merge(s1, s2);
|
||||
field.data.value = operator.noValue ? operator.default || true : undefined;
|
||||
data2value();
|
||||
},
|
||||
[data2value, field.data],
|
||||
);
|
||||
|
||||
const setRightValue = useCallback(
|
||||
(rightVar) => {
|
||||
field.data.rightVar = rightVar;
|
||||
data2value();
|
||||
},
|
||||
[data2value, field.data],
|
||||
);
|
||||
return {
|
||||
...(field?.data || {}),
|
||||
setLeftValue,
|
||||
setOperator,
|
||||
setRightValue,
|
||||
};
|
||||
};
|
@ -18,6 +18,8 @@ import {
|
||||
useDesigner,
|
||||
useFlag,
|
||||
useSchemaComponentContext,
|
||||
BlockContext,
|
||||
useBlockContext,
|
||||
} from '../../../';
|
||||
import { useToken } from '../__builtins__';
|
||||
import { designerCss } from './Table.Column.ActionBar';
|
||||
@ -77,6 +79,7 @@ export const TableColumnDecorator = (props) => {
|
||||
const compile = useCompile();
|
||||
const { isInSubTable } = useFlag() || {};
|
||||
const { token } = useToken();
|
||||
const { name } = useBlockContext?.() || {};
|
||||
|
||||
useEffect(() => {
|
||||
if (field.title) {
|
||||
@ -110,11 +113,13 @@ export const TableColumnDecorator = (props) => {
|
||||
})}
|
||||
>
|
||||
<CollectionFieldContext.Provider value={collectionField}>
|
||||
<Designer fieldSchema={fieldSchema} uiSchema={uiSchema} collectionField={collectionField} />
|
||||
<span role="button">
|
||||
{fieldSchema?.required && <span className="ant-formily-item-asterisk">*</span>}
|
||||
<span>{field?.title || compile(uiSchema?.title)}</span>
|
||||
</span>
|
||||
<BlockContext.Provider value={{ name: isInSubTable ? name : 'taleColumn' }}>
|
||||
<Designer fieldSchema={fieldSchema} uiSchema={uiSchema} collectionField={collectionField} />
|
||||
<span role="button">
|
||||
{fieldSchema?.required && <span className="ant-formily-item-asterisk">*</span>}
|
||||
<span>{field?.title || compile(uiSchema?.title)}</span>
|
||||
</span>
|
||||
</BlockContext.Provider>
|
||||
</CollectionFieldContext.Provider>
|
||||
</SortableItem>
|
||||
);
|
||||
|
@ -186,6 +186,7 @@ export type VariableInputProps = {
|
||||
className?: string;
|
||||
parseOptions?: ParseOptions;
|
||||
hideVariableButton?: boolean;
|
||||
constantAbel?: boolean;
|
||||
};
|
||||
|
||||
export function Input(props: VariableInputProps) {
|
||||
@ -202,6 +203,7 @@ export function Input(props: VariableInputProps) {
|
||||
fieldNames,
|
||||
parseOptions,
|
||||
hideVariableButton,
|
||||
constantAbel = true,
|
||||
} = props;
|
||||
const scope = typeof props.scope === 'function' ? props.scope() : props.scope;
|
||||
const { wrapSSR, hashId, componentCls, rootPrefixCls } = useStyles({ hideVariableButton });
|
||||
@ -233,6 +235,7 @@ export function Input(props: VariableInputProps) {
|
||||
);
|
||||
|
||||
const constantOption: DefaultOptionType & { component?: React.FC<any> } = useMemo(() => {
|
||||
if (!constantAbel) return null;
|
||||
if (children) {
|
||||
return {
|
||||
value: '$',
|
||||
|
@ -57,6 +57,7 @@ export const getTargetField = (obj) => {
|
||||
}
|
||||
});
|
||||
const result = keys.slice(0, index);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@ -76,72 +77,44 @@ function getAllKeys(obj) {
|
||||
return keys;
|
||||
}
|
||||
|
||||
const parseVariableValue = async (targetVariable, variables, localVariables) => {
|
||||
const parsingResult = isVariable(targetVariable)
|
||||
? [variables.parseVariable(targetVariable, localVariables).then(({ value }) => value)]
|
||||
: [targetVariable];
|
||||
|
||||
try {
|
||||
const [value] = await Promise.all(parsingResult);
|
||||
return value;
|
||||
} catch (error) {
|
||||
console.error('Error in parseVariableValue:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const conditionAnalyses = async (
|
||||
{
|
||||
ruleGroup,
|
||||
variables,
|
||||
localVariables,
|
||||
variableNameOfLeftCondition,
|
||||
conditionType,
|
||||
}: {
|
||||
ruleGroup;
|
||||
variables: VariablesContextType;
|
||||
localVariables: VariableOption[];
|
||||
/**
|
||||
* used to parse the variable name of the left condition value
|
||||
* @default '$nForm'
|
||||
*/
|
||||
variableNameOfLeftCondition?: string;
|
||||
conditionType?: 'advanced' | 'basic';
|
||||
},
|
||||
jsonLogic: any,
|
||||
) => {
|
||||
const type = Object.keys(ruleGroup)[0] || '$and';
|
||||
const conditions = ruleGroup[type];
|
||||
let results = conditions.map(async (condition) => {
|
||||
if ('$and' in condition || '$or' in condition) {
|
||||
return await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic);
|
||||
}
|
||||
|
||||
const logicCalculation = getInnermostKeyAndValue(condition);
|
||||
const operator = logicCalculation?.key;
|
||||
|
||||
if (!operator) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const targetVariableName = targetFieldToVariableString(getTargetField(condition), variableNameOfLeftCondition);
|
||||
const targetValue = variables
|
||||
.parseVariable(targetVariableName, localVariables, {
|
||||
doNotRequest: true,
|
||||
})
|
||||
.then(({ value }) => value);
|
||||
|
||||
const parsingResult = isVariable(logicCalculation?.value)
|
||||
? [variables.parseVariable(logicCalculation?.value, localVariables).then(({ value }) => value), targetValue]
|
||||
: [logicCalculation?.value, targetValue];
|
||||
|
||||
try {
|
||||
const [value, targetValue] = await Promise.all(parsingResult);
|
||||
const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables);
|
||||
let currentInputValue = transformVariableValue(targetValue, { targetCollectionField });
|
||||
const comparisonValue = transformVariableValue(value, { targetCollectionField });
|
||||
if (
|
||||
targetCollectionField?.type &&
|
||||
['datetime', 'date', 'datetimeNoTz', 'dateOnly', 'unixTimestamp'].includes(targetCollectionField.type) &&
|
||||
currentInputValue
|
||||
) {
|
||||
const picker = inferPickerType(comparisonValue);
|
||||
const format = getPickerFormat(picker);
|
||||
currentInputValue = dayjs(currentInputValue).format(format);
|
||||
}
|
||||
|
||||
return jsonLogic.apply({
|
||||
[operator]: [currentInputValue, comparisonValue],
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
results = await Promise.all(results);
|
||||
const results = await Promise.all(
|
||||
conditions.map((condition) =>
|
||||
processCondition(condition, variables, localVariables, variableNameOfLeftCondition, conditionType, jsonLogic),
|
||||
),
|
||||
);
|
||||
|
||||
if (type === '$and') {
|
||||
return every(results, (v) => v);
|
||||
@ -153,6 +126,67 @@ export const conditionAnalyses = async (
|
||||
}
|
||||
};
|
||||
|
||||
const processCondition = async (
|
||||
condition,
|
||||
variables,
|
||||
localVariables,
|
||||
variableNameOfLeftCondition,
|
||||
conditionType,
|
||||
jsonLogic,
|
||||
) => {
|
||||
if ('$and' in condition || '$or' in condition) {
|
||||
return await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic);
|
||||
}
|
||||
return conditionType === 'advanced'
|
||||
? processAdvancedCondition(condition, variables, localVariables, jsonLogic)
|
||||
: processBasicCondition(condition, variables, localVariables, variableNameOfLeftCondition, jsonLogic);
|
||||
};
|
||||
|
||||
const processAdvancedCondition = async (condition, variables, localVariables, jsonLogic) => {
|
||||
const operator = condition.op;
|
||||
const rightValue = await parseVariableValue(condition.rightVar, variables, localVariables);
|
||||
const leftValue = await parseVariableValue(condition.leftVar, variables, localVariables);
|
||||
if (operator) {
|
||||
return jsonLogic.apply({ [operator]: [leftValue, rightValue] });
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const processBasicCondition = async (condition, variables, localVariables, variableNameOfLeftCondition, jsonLogic) => {
|
||||
const logicCalculation = getInnermostKeyAndValue(condition);
|
||||
const operator = logicCalculation?.key;
|
||||
if (!operator) return true;
|
||||
|
||||
const targetVariableName = targetFieldToVariableString(getTargetField(condition), variableNameOfLeftCondition);
|
||||
const targetValue = variables
|
||||
.parseVariable(targetVariableName, localVariables, { doNotRequest: true })
|
||||
.then(({ value }) => value);
|
||||
|
||||
const parsingResult = isVariable(logicCalculation?.value)
|
||||
? [variables.parseVariable(logicCalculation?.value, localVariables).then(({ value }) => value), targetValue]
|
||||
: [logicCalculation?.value, targetValue];
|
||||
|
||||
try {
|
||||
const [value, resolvedTargetValue] = await Promise.all(parsingResult);
|
||||
const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables);
|
||||
let currentInputValue = transformVariableValue(resolvedTargetValue, { targetCollectionField });
|
||||
const comparisonValue = transformVariableValue(value, { targetCollectionField });
|
||||
|
||||
if (
|
||||
targetCollectionField?.type &&
|
||||
['datetime', 'date', 'datetimeNoTz', 'dateOnly', 'unixTimestamp'].includes(targetCollectionField.type) &&
|
||||
currentInputValue
|
||||
) {
|
||||
const picker = inferPickerType(comparisonValue);
|
||||
const format = getPickerFormat(picker);
|
||||
currentInputValue = dayjs(currentInputValue).format(format);
|
||||
}
|
||||
return jsonLogic.apply({ [operator]: [currentInputValue, comparisonValue] });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 转化成变量字符串,方便解析出值
|
||||
* @param targetField
|
||||
|
@ -89,6 +89,7 @@ const InternalCreateRecordAction = (props: any, ref) => {
|
||||
condition: v.condition,
|
||||
variables,
|
||||
localVariables,
|
||||
conditionType: v.conditionType,
|
||||
},
|
||||
app.jsonLogic,
|
||||
);
|
||||
@ -208,6 +209,7 @@ export const CreateAction = observer(
|
||||
condition: v.condition,
|
||||
variables,
|
||||
localVariables,
|
||||
conditionType: v.conditionType,
|
||||
},
|
||||
app.jsonLogic,
|
||||
);
|
||||
|
@ -26,7 +26,7 @@ import { VariableInput, getShouldChange } from '../../../schema-settings/Variabl
|
||||
import { Option } from '../../../schema-settings/VariableInput/type';
|
||||
import { formatVariableScop } from '../../../schema-settings/VariableInput/utils/formatVariableScop';
|
||||
import { useLocalVariables, useVariables } from '../../../variables';
|
||||
|
||||
import { BlockContext, useBlockContext } from '../../../block-provider';
|
||||
interface AssignedFieldProps {
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
@ -93,7 +93,7 @@ export enum AssignedFieldValueType {
|
||||
DynamicValue = 'dynamicValue',
|
||||
}
|
||||
|
||||
export const AssignedField = (props: AssignedFieldProps) => {
|
||||
export const AssignedFieldInner = (props: AssignedFieldProps) => {
|
||||
const { value, onChange } = props;
|
||||
const { getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager_deprecated();
|
||||
const collection = useCollection_deprecated();
|
||||
@ -148,3 +148,13 @@ export const AssignedField = (props: AssignedFieldProps) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AssignedField = (props) => {
|
||||
const { form } = useFormBlockContext();
|
||||
const { name } = useBlockContext();
|
||||
return (
|
||||
<BlockContext.Provider value={{ name: form ? 'form' : name }}>
|
||||
<AssignedFieldInner {...props} />
|
||||
</BlockContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -38,6 +38,7 @@ interface Props {
|
||||
*/
|
||||
variableNameOfLeftCondition?: string;
|
||||
action?: any;
|
||||
conditionType?: 'advanced' | 'basic';
|
||||
}
|
||||
|
||||
export function bindLinkageRulesToFiled(
|
||||
@ -83,7 +84,6 @@ export function bindLinkageRulesToFiled(
|
||||
() => {
|
||||
// 获取条件中的字段值
|
||||
const fieldValuesInCondition = getFieldValuesInCondition({ linkageRules, formValues });
|
||||
|
||||
// 获取条件中的变量值
|
||||
const variableValuesInCondition = getVariableValuesInCondition({ linkageRules, localVariables });
|
||||
|
||||
@ -132,20 +132,37 @@ function getVariableValuesInCondition({
|
||||
return linkageRules.map((rule) => {
|
||||
const type = Object.keys(rule.condition)[0] || '$and';
|
||||
const conditions = rule.condition[type];
|
||||
if (rule.conditionType === 'advanced') {
|
||||
return conditions
|
||||
.map((condition) => {
|
||||
if (!condition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return conditions
|
||||
.map((condition) => {
|
||||
const jsonlogic = getInnermostKeyAndValue(condition);
|
||||
if (!jsonlogic) {
|
||||
return null;
|
||||
}
|
||||
if (isVariable(jsonlogic.value)) {
|
||||
return getVariableValue(jsonlogic.value, localVariables);
|
||||
}
|
||||
const resolveVariable = (varName) =>
|
||||
isVariable(varName) ? getVariableValue(varName, localVariables) : varName;
|
||||
|
||||
return jsonlogic.value;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return {
|
||||
leftVar: resolveVariable(condition.leftVar),
|
||||
rightVar: resolveVariable(condition.rightVar),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
} else {
|
||||
return conditions
|
||||
.map((condition) => {
|
||||
const jsonlogic = getInnermostKeyAndValue(condition);
|
||||
if (!jsonlogic) {
|
||||
return null;
|
||||
}
|
||||
if (isVariable(jsonlogic.value)) {
|
||||
return getVariableValue(jsonlogic.value, localVariables);
|
||||
}
|
||||
|
||||
return jsonlogic.value;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -216,6 +233,7 @@ function getSubscriber(
|
||||
localVariables,
|
||||
variableNameOfLeftCondition,
|
||||
action,
|
||||
conditionType: rule.conditionType,
|
||||
},
|
||||
jsonLogic,
|
||||
);
|
||||
@ -327,7 +345,17 @@ function getFieldNameByOperator(operator: ActionType) {
|
||||
}
|
||||
|
||||
export const collectFieldStateOfLinkageRules = (
|
||||
{ operator, value, field, condition, variables, localVariables, variableNameOfLeftCondition, action }: Props,
|
||||
{
|
||||
operator,
|
||||
value,
|
||||
field,
|
||||
condition,
|
||||
variables,
|
||||
localVariables,
|
||||
variableNameOfLeftCondition,
|
||||
action,
|
||||
conditionType,
|
||||
}: Props,
|
||||
jsonLogic: any,
|
||||
) => {
|
||||
const requiredResult = field?.stateOfLinkageRules?.required || [field?.initStateOfLinkageRules?.required];
|
||||
@ -336,7 +364,13 @@ export const collectFieldStateOfLinkageRules = (
|
||||
const valueResult = field?.stateOfLinkageRules?.value || [field?.initStateOfLinkageRules?.value];
|
||||
const optionsResult = field?.stateOfLinkageRules?.dataSource || [field?.initStateOfLinkageRules?.dataSource];
|
||||
const { evaluate } = evaluators.get('formula.js');
|
||||
const paramsToGetConditionResult = { ruleGroup: condition, variables, localVariables, variableNameOfLeftCondition };
|
||||
const paramsToGetConditionResult = {
|
||||
ruleGroup: condition,
|
||||
variables,
|
||||
localVariables,
|
||||
variableNameOfLeftCondition,
|
||||
conditionType,
|
||||
};
|
||||
const dateScopeResult = field?.stateOfLinkageRules?.dateScope || [field?.initStateOfLinkageRules?.dateScope];
|
||||
|
||||
switch (operator) {
|
||||
|
@ -38,7 +38,12 @@ const getSatisfiedActions = async ({ rules, variables, localVariables }, jsonLog
|
||||
rules
|
||||
.filter((k) => !k.disabled)
|
||||
.map(async (rule) => {
|
||||
if (await conditionAnalyses({ ruleGroup: rule.condition, variables, localVariables }, jsonLogic)) {
|
||||
if (
|
||||
await conditionAnalyses(
|
||||
{ ruleGroup: rule.condition, variables, localVariables, conditionType: rule.conditionType },
|
||||
jsonLogic,
|
||||
)
|
||||
) {
|
||||
return rule;
|
||||
} else return null;
|
||||
}),
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { observer, useFieldSchema } from '@formily/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useCollectionManager_deprecated } from '../../collection-manager';
|
||||
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager';
|
||||
import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider';
|
||||
import { CollectionProvider } from '../../data-source/collection/CollectionProvider';
|
||||
import { withDynamicSchemaProps } from '../../hoc/withDynamicSchemaProps';
|
||||
@ -27,15 +27,75 @@ import { ArrayCollapse } from './components/LinkageHeader';
|
||||
export interface Props {
|
||||
dynamicComponent: any;
|
||||
}
|
||||
function extractFieldPath(obj, path = []) {
|
||||
if (typeof obj !== 'object' || obj === null) return null;
|
||||
|
||||
const [key, value] = Object.entries(obj)[0] || [];
|
||||
|
||||
if (typeof value === 'object' && value !== null && !key.startsWith('$')) {
|
||||
return extractFieldPath(value, [...path, key]);
|
||||
}
|
||||
|
||||
return [path.join('.'), obj];
|
||||
}
|
||||
type Condition = { [field: string]: { [op: string]: any } } | { $and: Condition[] } | { $or: Condition[] };
|
||||
type TransformedCondition =
|
||||
| { leftVar: string; op: string; rightVar: any }
|
||||
| { $and: TransformedCondition[] }
|
||||
| { $or: TransformedCondition[] };
|
||||
function transformConditionData(condition: Condition, variableKey: '$nForm' | '$nRecord'): TransformedCondition {
|
||||
if ('$and' in condition) {
|
||||
return {
|
||||
$and: condition.$and.map((c) => transformConditionData(c, variableKey)),
|
||||
};
|
||||
}
|
||||
|
||||
if ('$or' in condition) {
|
||||
return {
|
||||
$or: condition.$or.map((c) => transformConditionData(c, variableKey)),
|
||||
};
|
||||
}
|
||||
const [field, expression] = extractFieldPath(condition || {}) || [];
|
||||
|
||||
const [op, value] = Object.entries(expression || {})[0] || [];
|
||||
return {
|
||||
leftVar: field ? `{{${variableKey}.${field}}}` : null,
|
||||
op,
|
||||
rightVar: value,
|
||||
};
|
||||
}
|
||||
function getActiveContextName(contextList: { name: string; ctx: any }[]): string | null {
|
||||
const priority = ['$nForm', '$nRecord'];
|
||||
for (const name of priority) {
|
||||
const item = contextList.find((ctx) => ctx.name === name && ctx.ctx);
|
||||
if (item) return name;
|
||||
}
|
||||
return '$nRecord';
|
||||
}
|
||||
|
||||
const transformDefaultValue = (values, variableKey) => {
|
||||
return values.map((v) => {
|
||||
if (v.conditionType !== 'advanced') {
|
||||
const condition = transformConditionData(v.condition, variableKey);
|
||||
return {
|
||||
...v,
|
||||
condition: variableKey ? condition : v.condition,
|
||||
conditionType: variableKey ? 'advanced' : 'basic',
|
||||
};
|
||||
}
|
||||
return v;
|
||||
});
|
||||
};
|
||||
|
||||
export const FormLinkageRules = withDynamicSchemaProps(
|
||||
observer((props: Props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { options, defaultValues, collectionName, form, variables, localVariables, record, dynamicComponent } =
|
||||
useProps(props); // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const { name } = useCollection_deprecated();
|
||||
const { getAllCollectionsInheritChain } = useCollectionManager_deprecated();
|
||||
const parentRecordData = useCollectionParentRecordData();
|
||||
|
||||
const variableKey = getActiveContextName(localVariables);
|
||||
const components = useMemo(() => ({ ArrayCollapse }), []);
|
||||
const schema = useMemo(
|
||||
() => ({
|
||||
@ -43,7 +103,7 @@ export const FormLinkageRules = withDynamicSchemaProps(
|
||||
properties: {
|
||||
rules: {
|
||||
type: 'array',
|
||||
default: defaultValues,
|
||||
default: transformDefaultValue(defaultValues, variableKey),
|
||||
'x-component': 'ArrayCollapse',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': {
|
||||
@ -72,6 +132,20 @@ export const FormLinkageRules = withDynamicSchemaProps(
|
||||
'x-content': '{{ t("Condition") }}',
|
||||
},
|
||||
condition: {
|
||||
'x-component': 'Input', // 仅作为数据存储
|
||||
'x-hidden': true, // 不显示
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.conditionType', '.conditionBasic', '.conditionAdvanced'],
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$deps[0] === "basic" ? $deps[1] : $deps[2]}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
conditionBasic: {
|
||||
'x-component': 'Filter',
|
||||
'x-use-component-props': () => {
|
||||
return {
|
||||
@ -83,6 +157,7 @@ export const FormLinkageRules = withDynamicSchemaProps(
|
||||
`,
|
||||
};
|
||||
},
|
||||
'x-visible': '{{$deps[0] === "basic"}}',
|
||||
'x-component-props': {
|
||||
collectionName,
|
||||
dynamicComponent: (props: DynamicComponentProps) => {
|
||||
@ -102,6 +177,38 @@ export const FormLinkageRules = withDynamicSchemaProps(
|
||||
);
|
||||
},
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.conditionType', '.condition'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{$deps[0] === "basic"}}',
|
||||
value: '{{$deps[0] === "basic" ? $deps[1] : undefined}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
conditionAdvanced: {
|
||||
'x-component': 'LinkageFilter',
|
||||
'x-visible': '{{$deps[0] === "advanced"}}',
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.conditionType', '.condition'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{$deps[0] === "advanced"}}',
|
||||
value: '{{$deps[0] === "advanced" ? $deps[1] : undefined}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
conditionType: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
default: 'advanced',
|
||||
'x-hidden': true,
|
||||
},
|
||||
actions: {
|
||||
'x-component': 'h4',
|
||||
@ -168,10 +275,10 @@ export const FormLinkageRules = withDynamicSchemaProps(
|
||||
|
||||
return (
|
||||
// 这里使用 SubFormProvider 包裹,是为了让子表格的联动规则中 “当前对象” 的配置显示正确
|
||||
<SubFormProvider value={{ value: null, collection: { name: collectionName } as any }}>
|
||||
<SubFormProvider value={{ value: null, collection: { name: collectionName || name } as any }}>
|
||||
<RecordProvider record={record} parent={parentRecordData}>
|
||||
<FilterContext.Provider value={value}>
|
||||
<CollectionProvider name={collectionName}>
|
||||
<CollectionProvider name={collectionName || name} allowNull>
|
||||
<SchemaComponent components={components} schema={schema} />
|
||||
</CollectionProvider>
|
||||
</FilterContext.Provider>
|
||||
|
@ -32,9 +32,11 @@ export enum ActionType {
|
||||
export enum LinkageRuleCategory {
|
||||
default = 'default',
|
||||
style = 'style',
|
||||
button = 'button',
|
||||
}
|
||||
|
||||
export const LinkageRuleDataKeyMap: Record<`${LinkageRuleCategory}`, string> = {
|
||||
[LinkageRuleCategory.style]: 'x-linkage-style-rules',
|
||||
[LinkageRuleCategory.default]: 'x-linkage-rules',
|
||||
[LinkageRuleCategory.button]: 'x-linkage-rules',
|
||||
};
|
||||
|
@ -1122,7 +1122,7 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) {
|
||||
const getRules = useCallback(() => {
|
||||
return gridSchema?.[dataKey] || fieldSchema?.[dataKey] || [];
|
||||
}, [gridSchema, fieldSchema, dataKey]);
|
||||
const title = titleMap[category];
|
||||
const title = titleMap[category] || t('Linkage rules');
|
||||
const schema = useMemo<ISchema>(
|
||||
() => ({
|
||||
type: 'object',
|
||||
@ -1155,7 +1155,7 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) {
|
||||
(v) => {
|
||||
const rules = [];
|
||||
for (const rule of v.fieldReaction.rules) {
|
||||
rules.push(_.pickBy(rule, _.identity));
|
||||
rules.push(_.omit(_.pickBy(rule, _.identity), ['conditionBasic', 'conditionAdvanced']));
|
||||
}
|
||||
const templateId = gridSchema['x-component'] === 'BlockTemplate' && gridSchema['x-component-props']?.templateId;
|
||||
const uid = (templateId && getTemplateById(templateId).uid) || gridSchema['x-uid'];
|
||||
|
@ -11,7 +11,7 @@ import { Form } from '@formily/core';
|
||||
// @ts-ignore
|
||||
import { Schema } from '@formily/json-schema';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CollectionFieldOptions_deprecated } from '../../collection-manager';
|
||||
import { Variable, useVariableScope } from '../../schema-component';
|
||||
@ -72,6 +72,10 @@ type Props = {
|
||||
*/
|
||||
noDisabled?: boolean;
|
||||
hideVariableButton?: boolean;
|
||||
setScopes?: any; //更新scopes
|
||||
nullable?: boolean;
|
||||
constantAbel?: boolean;
|
||||
changeOnSelect?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -98,6 +102,10 @@ export const VariableInput = (props: Props) => {
|
||||
targetFieldSchema,
|
||||
noDisabled,
|
||||
hideVariableButton,
|
||||
setScopes,
|
||||
nullable = true,
|
||||
constantAbel = true,
|
||||
changeOnSelect = true,
|
||||
} = props;
|
||||
const { name: blockCollectionName } = useBlockCollection();
|
||||
const scope = useVariableScope();
|
||||
@ -127,31 +135,37 @@ export const VariableInput = (props: Props) => {
|
||||
const handleChange = useCallback(
|
||||
(value: any, optionPath: any[]) => {
|
||||
if (!shouldChange) {
|
||||
return onChange(value);
|
||||
return onChange(value, optionPath);
|
||||
}
|
||||
|
||||
// `shouldChange` 这个函数的运算量比较大,会导致展开变量列表时有明显的卡顿感,在这里加个延迟能有效解决这个问题
|
||||
setTimeout(async () => {
|
||||
if (await shouldChange(value, optionPath)) {
|
||||
onChange(value);
|
||||
onChange(value, optionPath);
|
||||
}
|
||||
});
|
||||
},
|
||||
[onChange, shouldChange],
|
||||
);
|
||||
const scopes = returnScope(
|
||||
compatOldVariables(_.isEmpty(scope) ? variableOptions : scope, {
|
||||
value,
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
setScopes?.(scopes);
|
||||
}, [value, scope]);
|
||||
return (
|
||||
<Variable.Input
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
scope={returnScope(
|
||||
compatOldVariables(_.isEmpty(scope) ? variableOptions : scope, {
|
||||
value,
|
||||
}),
|
||||
)}
|
||||
scope={scopes}
|
||||
style={style}
|
||||
changeOnSelect
|
||||
changeOnSelect={changeOnSelect}
|
||||
hideVariableButton={hideVariableButton}
|
||||
nullable={nullable}
|
||||
constantAbel={constantAbel}
|
||||
>
|
||||
<RenderSchemaComponent value={value} onChange={onChange} />
|
||||
</Variable.Input>
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
import { useAPIClient } from '../../../api-client/hooks/useAPIClient';
|
||||
import { useBaseVariable } from './useBaseVariable';
|
||||
import { string } from '../../../collection-manager/interfaces/properties/operators';
|
||||
|
||||
/**
|
||||
* 变量:`当前 Token`
|
||||
@ -26,6 +27,7 @@ export const useAPITokenVariable = ({
|
||||
title: 'API token',
|
||||
noDisabled,
|
||||
noChildren: true,
|
||||
operators: string,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -87,6 +87,8 @@ interface BaseProps {
|
||||
*/
|
||||
deprecated?: boolean;
|
||||
tooltip?: string;
|
||||
/**支持的操作符 */
|
||||
operators?: any[];
|
||||
}
|
||||
|
||||
interface BaseVariableProviderProps {
|
||||
@ -133,6 +135,8 @@ const getChildren = (
|
||||
: isDisabled({ option, collectionField, uiSchema, targetFieldSchema, getCollectionField })),
|
||||
isLeaf: true,
|
||||
depth,
|
||||
operators: option?.operators,
|
||||
schema: option?.schema,
|
||||
};
|
||||
}
|
||||
|
||||
@ -197,6 +201,7 @@ export const useBaseVariable = ({
|
||||
returnFields = (fields) => fields,
|
||||
deprecated,
|
||||
tooltip,
|
||||
operators = [],
|
||||
}: BaseProps) => {
|
||||
const compile = useCompile();
|
||||
const getFilterOptions = useGetFilterOptions();
|
||||
@ -276,6 +281,7 @@ export const useBaseVariable = ({
|
||||
children: [],
|
||||
disabled: !!deprecated,
|
||||
deprecated,
|
||||
operators,
|
||||
} as Option;
|
||||
}, [uiSchema?.['x-component']]);
|
||||
|
||||
|
@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useOperators } from '../../../block-provider/CollectOperators';
|
||||
import { useDatePickerContext } from '../../../schema-component/antd/date-picker/DatePicker';
|
||||
import { getDateRanges } from '../../../schema-component/antd/date-picker/util';
|
||||
|
||||
import { datetime } from '../../../collection-manager/interfaces/properties/operators';
|
||||
interface Props {
|
||||
operator?: {
|
||||
value: string;
|
||||
@ -45,132 +45,155 @@ export const useDateVariable = ({ operator, schema, noDisabled }: Props) => {
|
||||
value: 'now',
|
||||
label: t('Current time'),
|
||||
disabled: noDisabled ? false : schema?.['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween',
|
||||
operators: datetime,
|
||||
schema: {},
|
||||
},
|
||||
{
|
||||
key: 'yesterday',
|
||||
value: 'yesterday',
|
||||
label: t('Yesterday'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'today',
|
||||
value: 'today',
|
||||
label: t('Today'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'tomorrow',
|
||||
value: 'tomorrow',
|
||||
label: t('Tomorrow'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'lastIsoWeek',
|
||||
value: 'lastIsoWeek',
|
||||
label: t('Last week'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'thisIsoWeek',
|
||||
value: 'thisIsoWeek',
|
||||
label: t('This week'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'nextIsoWeek',
|
||||
value: 'nextIsoWeek',
|
||||
label: t('Next week'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'lastMonth',
|
||||
value: 'lastMonth',
|
||||
label: t('Last month'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'thisMonth',
|
||||
value: 'thisMonth',
|
||||
label: t('This month'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'nextMonth',
|
||||
value: 'nextMonth',
|
||||
label: t('Next month'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'lastQuarter',
|
||||
value: 'lastQuarter',
|
||||
label: t('Last quarter'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'thisQuarter',
|
||||
value: 'thisQuarter',
|
||||
label: t('This quarter'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'nextQuarter',
|
||||
value: 'nextQuarter',
|
||||
label: t('Next quarter'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'lastYear',
|
||||
value: 'lastYear',
|
||||
label: t('Last year'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'thisYear',
|
||||
value: 'thisYear',
|
||||
label: t('This year'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'nextYear',
|
||||
value: 'nextYear',
|
||||
label: t('Next year'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'last7Days',
|
||||
value: 'last7Days',
|
||||
label: t('Last 7 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'next7Days',
|
||||
value: 'next7Days',
|
||||
label: t('Next 7 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'last30Days',
|
||||
value: 'last30Days',
|
||||
label: t('Last 30 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'next30Days',
|
||||
value: 'next30Days',
|
||||
label: t('Next 30 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'last90Days',
|
||||
value: 'last90Days',
|
||||
label: t('Last 90 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'next90Days',
|
||||
value: 'next90Days',
|
||||
label: t('Next 90 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
];
|
||||
|
||||
@ -222,132 +245,154 @@ export const useDatetimeVariable = ({ operator, schema, noDisabled, targetFieldS
|
||||
value: 'now',
|
||||
label: t('Current time'),
|
||||
disabled: noDisabled ? false : schema?.['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween',
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'yesterday',
|
||||
value: 'yesterday',
|
||||
label: t('Yesterday'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'today',
|
||||
value: 'today',
|
||||
label: t('Today'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'tomorrow',
|
||||
value: 'tomorrow',
|
||||
label: t('Tomorrow'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'lastIsoWeek',
|
||||
value: 'lastIsoWeek',
|
||||
label: t('Last week'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'thisIsoWeek',
|
||||
value: 'thisIsoWeek',
|
||||
label: t('This week'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'nextIsoWeek',
|
||||
value: 'nextIsoWeek',
|
||||
label: t('Next week'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'lastMonth',
|
||||
value: 'lastMonth',
|
||||
label: t('Last month'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'thisMonth',
|
||||
value: 'thisMonth',
|
||||
label: t('This month'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'nextMonth',
|
||||
value: 'nextMonth',
|
||||
label: t('Next month'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'lastQuarter',
|
||||
value: 'lastQuarter',
|
||||
label: t('Last quarter'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'thisQuarter',
|
||||
value: 'thisQuarter',
|
||||
label: t('This quarter'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'nextQuarter',
|
||||
value: 'nextQuarter',
|
||||
label: t('Next quarter'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'lastYear',
|
||||
value: 'lastYear',
|
||||
label: t('Last year'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'thisYear',
|
||||
value: 'thisYear',
|
||||
label: t('This year'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'nextYear',
|
||||
value: 'nextYear',
|
||||
label: t('Next year'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'last7Days',
|
||||
value: 'last7Days',
|
||||
label: t('Last 7 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'next7Days',
|
||||
value: 'next7Days',
|
||||
label: t('Next 7 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'last30Days',
|
||||
value: 'last30Days',
|
||||
label: t('Last 30 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'next30Days',
|
||||
value: 'next30Days',
|
||||
label: t('Next 30 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'last90Days',
|
||||
value: 'last90Days',
|
||||
label: t('Last 90 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
{
|
||||
key: 'next90Days',
|
||||
value: 'next90Days',
|
||||
label: t('Next 90 days'),
|
||||
disabled,
|
||||
operators: datetime,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
import { Form } from '@formily/core';
|
||||
import { Schema } from '@formily/json-schema';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBlockContext } from '../../../block-provider';
|
||||
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
||||
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
|
||||
import { useDataBlockRequestData, useDataSource } from '../../../data-source';
|
||||
@ -62,14 +63,6 @@ export const useFormVariable = ({ collectionName, collectionField, schema, noDis
|
||||
return result;
|
||||
};
|
||||
|
||||
const useCurrentFormData = () => {
|
||||
const data = useDataBlockRequestData();
|
||||
if (data?.data?.length > 1) {
|
||||
return;
|
||||
}
|
||||
return data?.data?.[0] || data?.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* 变量:`当前表单` 相关的 hook
|
||||
* @param param0
|
||||
@ -78,14 +71,14 @@ const useCurrentFormData = () => {
|
||||
export const useCurrentFormContext = ({ form: _form }: Pick<Props, 'form'> = {}) => {
|
||||
const { form } = useFormBlockContext();
|
||||
const { isVariableParsedInOtherContext } = useFlag();
|
||||
|
||||
const { name } = useBlockContext?.() || {};
|
||||
const formInstance = _form || form;
|
||||
|
||||
return {
|
||||
/** 变量值 */
|
||||
currentFormCtx: formInstance?.values,
|
||||
/** 用来判断是否可以显示`当前表单`变量 */
|
||||
shouldDisplayCurrentForm: formInstance && !formInstance.readPretty && !isVariableParsedInOtherContext,
|
||||
shouldDisplayCurrentForm:
|
||||
name === 'form' && formInstance && !formInstance.readPretty && !isVariableParsedInOtherContext,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -90,7 +90,10 @@ export const useCurrentRecordContext = () => {
|
||||
/** 变量值 */
|
||||
currentRecordCtx: ctx?.recordData || formRecord?.data || recordData,
|
||||
/** 用于判断是否需要显示配置项 */
|
||||
shouldDisplayCurrentRecord: !_.isEmpty(_.omit(recordData, ['__collectionName', '__parent'])) || !!formRecord?.data,
|
||||
shouldDisplayCurrentRecord:
|
||||
!_.isEmpty(_.omit(recordData, ['__collectionName', '__parent'])) ||
|
||||
!!formRecord?.data ||
|
||||
blockType === 'taleColumn',
|
||||
/** 当前记录对应的 collection name */
|
||||
collectionName: realCollectionName,
|
||||
/** 块类型 */
|
||||
|
@ -13,6 +13,9 @@ import { useAPIClient } from '../../../api-client';
|
||||
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
|
||||
import { CollectionFieldOptions } from '../../../data-source/collection/Collection';
|
||||
import { useBaseVariable } from './useBaseVariable';
|
||||
import { string } from '../../../collection-manager/interfaces/properties/operators';
|
||||
import { useCurrentUserContext } from '../../../user/CurrentUserProvider';
|
||||
import { useCompile } from '../../../schema-component';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
@ -47,6 +50,7 @@ export const useRoleVariable = ({
|
||||
noDisabled,
|
||||
targetFieldSchema,
|
||||
noChildren: true,
|
||||
operators: string,
|
||||
});
|
||||
|
||||
return result;
|
||||
@ -73,6 +77,9 @@ export const useCurrentRoleVariable = ({
|
||||
} = {}) => {
|
||||
const { t } = useTranslation();
|
||||
const apiClient = useAPIClient();
|
||||
const compile = useCompile();
|
||||
const { data } = useCurrentUserContext() || {};
|
||||
const roles = (data?.data?.roles || []).map(({ name, title }) => ({ name, title: compile(title) }));
|
||||
const currentRoleSettings = useBaseVariable({
|
||||
collectionField,
|
||||
uiSchema,
|
||||
@ -83,12 +90,13 @@ export const useCurrentRoleVariable = ({
|
||||
noDisabled,
|
||||
targetFieldSchema,
|
||||
noChildren: true,
|
||||
operators: string,
|
||||
});
|
||||
|
||||
return {
|
||||
/** 变量配置项 */
|
||||
currentRoleSettings,
|
||||
/** 变量的值 */
|
||||
currentRoleCtx: apiClient.auth?.role,
|
||||
currentRoleCtx: apiClient.auth?.role === '__union__' ? roles.map((v) => v.name) : apiClient.auth?.role,
|
||||
};
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ export const useIsLoggedIn = () => {
|
||||
|
||||
export const useCurrentRoles = () => {
|
||||
const { allowAnonymous } = useACLRoleContext();
|
||||
const { data } = useCurrentUserContext();
|
||||
const { data } = useCurrentUserContext() || {};
|
||||
const compile = useCompile();
|
||||
const options = useMemo(() => {
|
||||
const roles = (data?.data?.roles || []).map(({ name, title }) => ({ name, title: compile(title) }));
|
||||
|
@ -21,9 +21,10 @@ import {
|
||||
SecondConFirm,
|
||||
AfterSuccess,
|
||||
RefreshDataBlockRequest,
|
||||
SchemaSettingsLinkageRules,
|
||||
useDataBlockProps,
|
||||
} from '@nocobase/client';
|
||||
import { ModalProps } from 'antd';
|
||||
import { isValid } from '@formily/shared';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -96,6 +97,16 @@ export const deprecatedBulkEditActionSettings = new SchemaSettings({
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaInitializerOpenModeSchemaItems,
|
||||
@ -138,6 +149,16 @@ export const bulkEditActionSettings = new SchemaSettings({
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaInitializerOpenModeSchemaItems,
|
||||
@ -158,6 +179,7 @@ export const bulkEditActionSettings = new SchemaSettings({
|
||||
name: 'updateMode',
|
||||
Component: UpdateMode,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'remove',
|
||||
sort: 100,
|
||||
@ -191,6 +213,17 @@ export const bulkEditFormSubmitActionSettings = new SchemaSettings({
|
||||
name: 'afterSuccessfulSubmission',
|
||||
Component: AfterSuccess,
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'refreshDataBlockRequest',
|
||||
Component: RefreshDataBlockRequest,
|
||||
|
@ -22,6 +22,10 @@ import {
|
||||
useGlobalVariable,
|
||||
BlocksSelector,
|
||||
usePlugin,
|
||||
SchemaSettingsLinkageRules,
|
||||
useCollectionManager_deprecated,
|
||||
useDataBlockProps,
|
||||
useCollection_deprecated,
|
||||
} from '@nocobase/client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React from 'react';
|
||||
@ -161,6 +165,21 @@ const schemaSettingsItems: SchemaSettingsItemType[] = [
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { association } = useDataBlockProps() || {};
|
||||
const { getCollectionField } = useCollectionManager_deprecated();
|
||||
const associationField = getCollectionField(association);
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
collectionName: associationField?.collectionName || name,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'updateMode',
|
||||
Component: UpdateMode,
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
useCollectionRecordData,
|
||||
useCompile,
|
||||
useGlobalVariable,
|
||||
useFormBlockContext,
|
||||
} from '@nocobase/client';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from '../locale';
|
||||
@ -27,6 +28,7 @@ export const useCustomRequestVariableOptions = () => {
|
||||
const compile = useCompile();
|
||||
const recordData = useCollectionRecordData();
|
||||
const { name: blockType } = useBlockContext() || {};
|
||||
const { form } = useFormBlockContext();
|
||||
const [fields, userFields] = useMemo(() => {
|
||||
return [compile(fieldsOptions), compile(userFieldOptions)];
|
||||
}, [fieldsOptions, userFieldOptions]);
|
||||
@ -39,7 +41,7 @@ export const useCustomRequestVariableOptions = () => {
|
||||
title: t('Current record', { ns: 'client' }),
|
||||
children: [...fields],
|
||||
},
|
||||
blockType === 'form' && {
|
||||
(blockType === 'form' || form) && {
|
||||
name: '$nForm',
|
||||
title: t('Current form', { ns: 'client' }),
|
||||
children: [...fields],
|
||||
|
@ -20,6 +20,8 @@ import {
|
||||
useCollectionRecord,
|
||||
useSchemaToolbar,
|
||||
SchemaSettingAccessControl,
|
||||
useDataBlockProps,
|
||||
useCollectionManager_deprecated,
|
||||
} from '@nocobase/client';
|
||||
import { CustomRequestSettingsItem } from './components/CustomRequestActionDesigner';
|
||||
|
||||
@ -40,17 +42,11 @@ export const customizeCustomRequestActionSettings = new SchemaSettings({
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection() || {};
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
collectionName: name,
|
||||
};
|
||||
},
|
||||
useVisible() {
|
||||
const record = useCollectionRecord();
|
||||
return record && record.data && !record?.isNew;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'secondConFirm',
|
||||
|
@ -9,7 +9,14 @@
|
||||
|
||||
import { ArrayItems } from '@formily/antd-v5';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import { ButtonEditor, SchemaSettings, useDesignable, useSchemaToolbar } from '@nocobase/client';
|
||||
import {
|
||||
ButtonEditor,
|
||||
SchemaSettings,
|
||||
useDesignable,
|
||||
useSchemaToolbar,
|
||||
SchemaSettingsLinkageRules,
|
||||
useDataBlockProps,
|
||||
} from '@nocobase/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useShared } from './useShared';
|
||||
@ -25,6 +32,16 @@ export const exportActionSchemaSettings = new SchemaSettings({
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'exportableFields',
|
||||
type: 'actionModal',
|
||||
@ -65,6 +82,7 @@ export const exportActionSchemaSettings = new SchemaSettings({
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'divider',
|
||||
type: 'divider',
|
||||
|
@ -9,14 +9,19 @@
|
||||
|
||||
import { ArrayItems } from '@formily/antd-v5';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import { ButtonEditor, SchemaSettings, type, useDesignable, useSchemaToolbar } from '@nocobase/client';
|
||||
import {
|
||||
ButtonEditor,
|
||||
SchemaSettings,
|
||||
useDesignable,
|
||||
useSchemaToolbar,
|
||||
SchemaSettingsLinkageRules,
|
||||
} from '@nocobase/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useShared } from './useShared';
|
||||
import { Button, Space } from 'antd';
|
||||
import { Action } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useDownloadXlsxTemplateAction } from './useImportAction';
|
||||
|
||||
export const importActionSchemaSettings = new SchemaSettings({
|
||||
name: 'actionSettings:import',
|
||||
@ -29,6 +34,16 @@ export const importActionSchemaSettings = new SchemaSettings({
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'importableFields',
|
||||
type: 'actionModal',
|
||||
@ -72,6 +87,7 @@ export const importActionSchemaSettings = new SchemaSettings({
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'divider',
|
||||
type: 'divider',
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
SchemaSettings,
|
||||
SchemaSettingsItemType,
|
||||
SchemaSettingsLinkageRules,
|
||||
useCollection_deprecated,
|
||||
useSchemaToolbar,
|
||||
} from '@nocobase/client';
|
||||
|
||||
@ -35,11 +34,9 @@ const schemaSettingsItems: SchemaSettingsItemType[] = [
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
collectionName: name,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -14,6 +14,8 @@ import {
|
||||
useSchemaInitializer,
|
||||
ModalActionSchemaInitializerItem,
|
||||
SchemaSettingAccessControl,
|
||||
SchemaSettingsLinkageRules,
|
||||
useSchemaToolbar,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -27,6 +29,16 @@ export const workbenchActionSettingsCustomRequest = new SchemaSettings({
|
||||
return { hasIconColor: true };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'editLink',
|
||||
Component: SchemaSettingsActionLinkItem,
|
||||
|
@ -15,6 +15,8 @@ import {
|
||||
useSchemaInitializerItem,
|
||||
ModalActionSchemaInitializerItem,
|
||||
SchemaSettingAccessControl,
|
||||
SchemaSettingsLinkageRules,
|
||||
useSchemaToolbar,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -29,6 +31,16 @@ export const workbenchActionSettingsLink = new SchemaSettings({
|
||||
return { hasIconColor: true };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'editLink',
|
||||
Component: SchemaSettingsActionLinkItem,
|
||||
|
@ -15,6 +15,8 @@ import {
|
||||
useOpenModeContext,
|
||||
ModalActionSchemaInitializerItem,
|
||||
SchemaSettingAccessControl,
|
||||
SchemaSettingsLinkageRules,
|
||||
useSchemaToolbar,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -29,7 +31,16 @@ export const workbenchActionSettingsPopup = new SchemaSettings({
|
||||
return { hasIconColor: true };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
|
@ -15,6 +15,8 @@ import {
|
||||
useSchemaInitializerItem,
|
||||
ModalActionSchemaInitializerItem,
|
||||
SchemaSettingAccessControl,
|
||||
SchemaSettingsLinkageRules,
|
||||
useSchemaToolbar,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -29,6 +31,16 @@ export const workbenchActionSettingsScanQrCode = new SchemaSettings({
|
||||
return { hasIconColor: true };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { linkageRulesProps } = useSchemaToolbar();
|
||||
return {
|
||||
...linkageRulesProps,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
...SchemaSettingAccessControl,
|
||||
useVisible() {
|
||||
|
@ -20,7 +20,7 @@ import { useStorageUploadProps } from './useStorageUploadProps';
|
||||
|
||||
export const useUploadFiles = () => {
|
||||
const { getDataBlockRequest } = useDataBlockRequestGetter();
|
||||
const { association } = useDataBlockProps();
|
||||
const { association } = useDataBlockProps() || {};
|
||||
const { setVisible } = useActionContext();
|
||||
const collection = useCollection();
|
||||
const sourceId = useSourceId();
|
||||
|
Loading…
x
Reference in New Issue
Block a user