From 7d660f5a0fcdc93707472985ca463eef4bcd3c15 Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Fri, 19 Jul 2024 09:07:19 +0800 Subject: [PATCH 1/2] feat(Link): add 'Open in new window' option (#4898) * refactor: rename 'useURLAndParamsSchema' to 'useURLAndHTMLSchema' and improve it * feat: add 'Open in new window' option * test: add e2e test --- .../__tests__/hooks/index.test.ts | 87 ++++++++- .../client/src/block-provider/hooks/index.ts | 38 +++- packages/core/client/src/index.ts | 2 +- .../actions/__e2e__/link/basic.test.ts | 32 +++- .../modules/actions/__e2e__/link/templates.ts | 170 ++++++++++++++++++ .../link/customizeLinkActionSettings.tsx | 21 ++- ...ramsSchema.tsx => useURLAndHTMLSchema.tsx} | 59 +++++- .../client/src/modules/menu/LinkMenuItem.tsx | 4 +- .../antd/menu/Menu.Designer.tsx | 6 +- packages/core/utils/src/url.ts | 7 +- .../src/client/schemaSettings.tsx | 4 +- 11 files changed, 404 insertions(+), 26 deletions(-) rename packages/core/client/src/modules/actions/link/{useURLAndParamsSchema.tsx => useURLAndHTMLSchema.tsx} (73%) diff --git a/packages/core/client/src/block-provider/__tests__/hooks/index.test.ts b/packages/core/client/src/block-provider/__tests__/hooks/index.test.ts index 91cee10e72..574503de3c 100644 --- a/packages/core/client/src/block-provider/__tests__/hooks/index.test.ts +++ b/packages/core/client/src/block-provider/__tests__/hooks/index.test.ts @@ -7,7 +7,13 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { appendQueryStringToUrl, parseVariablesAndChangeParamsToQueryString, reduceValueSize } from '../../hooks/index'; +import { + appendQueryStringToUrl, + completeURL, + navigateWithinSelf, + parseVariablesAndChangeParamsToQueryString, + reduceValueSize, +} from '../../hooks/index'; describe('parseVariablesAndChangeParamsToQueryString', () => { it('should parse variables and change params to query string', async () => { @@ -150,3 +156,82 @@ describe('appendQueryStringToUrl', () => { expect(result).toBe('https://example.com?existingParam=value¶m1=value1¶m2=value2'); }); }); + +describe('completeURL', () => { + it('should complete a relative URL with the origin', () => { + const origin = 'https://example.com'; + const url = '/path/to/resource'; + + const result = completeURL(url, origin); + + expect(result).toBe('https://example.com/path/to/resource'); + }); + + it('should return the URL as is if it is already complete', () => { + const origin = 'https://example.com'; + const url = 'https://example.com/path/to/resource'; + + const result = completeURL(url, origin); + + expect(result).toBe('https://example.com/path/to/resource'); + }); + + it('should return the URL as is if it is already complete with a different origin', () => { + const origin = 'https://example.com'; + const url = 'https://another.com/path/to/resource'; + + const result = completeURL(url, origin); + + expect(result).toBe('https://another.com/path/to/resource'); + }); + + it('should return an empty string if the URL is falsy', () => { + const origin = 'https://example.com'; + const url = ''; + + const result = completeURL(url, origin); + + expect(result).toBe(''); + }); +}); + +describe('navigateWithinSelf', () => { + it('should navigate within the same window if the link is a relative URL', () => { + const origin = 'https://example.com'; + const link = '/path/to/resource'; + const navigate = vi.fn(); + + navigateWithinSelf(link, navigate, origin); + + expect(navigate).toHaveBeenCalledWith(link); + }); + + it('should navigate within the same window if the link starts with the origin', () => { + const origin = 'https://example.com'; + const link = 'https://example.com/path/to/resource'; + const navigate = vi.fn(); + + navigateWithinSelf(link, navigate, origin); + + expect(navigate).toHaveBeenCalledWith('/path/to/resource'); + }); + + it('should open the link in the same window if it is an external URL', () => { + const origin = 'https://example.com'; + const link = 'https://other.com/path/to/resource'; + window.open = vi.fn(); + + navigateWithinSelf(link, () => {}, origin); + + expect(window.open).toHaveBeenCalledWith(link, '_self'); + }); + + it('should log an error if the link is not a string', () => { + const link = null; + console.error = vi.fn(); + + navigateWithinSelf(link, () => {}); + + expect(console.error).toHaveBeenCalledWith('link should be a string'); + }); +}); diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index ca890b0e0d..b0a45a50f2 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -19,7 +19,7 @@ import omit from 'lodash/omit'; import qs from 'qs'; import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; +import { NavigateFunction, useNavigate } from 'react-router-dom'; import { useReactToPrint } from 'react-to-print'; import { AssociationFilter, @@ -1560,6 +1560,7 @@ export function useLinkActionProps() { const { t } = useTranslation(); const url = fieldSchema?.['x-component-props']?.['url']; const searchParams = fieldSchema?.['x-component-props']?.['params'] || []; + const openInNewWindow = fieldSchema?.['x-component-props']?.['openInNewWindow']; const { parseURLAndParams } = useParseURLAndParams(); return { @@ -1572,11 +1573,13 @@ export function useLinkActionProps() { const link = await parseURLAndParams(url, searchParams); if (link) { - if (isURL(link)) { - window.open(link, '_blank'); + if (openInNewWindow) { + window.open(completeURL(link), '_blank'); } else { - navigate(link); + navigateWithinSelf(link, navigate); } + } else { + console.error('link should be a string'); } }, }; @@ -1675,3 +1678,30 @@ export function reduceValueSize(value: any) { return value; } + +// 补全 URL +export function completeURL(url: string, origin = window.location.origin) { + if (!url) { + return ''; + } + if (isURL(url)) { + return url; + } + return url.startsWith('/') ? `${origin}${url}` : `${origin}/${url}`; +} + +export function navigateWithinSelf(link: string, navigate: NavigateFunction, origin = window.location.origin) { + if (!_.isString(link)) { + return console.error('link should be a string'); + } + + if (isURL(link)) { + if (link.startsWith(origin)) { + navigate(link.replace(origin, '')); + } else { + window.open(link, '_self'); + } + } else { + navigate(link.startsWith('/') ? link : `/${link}`); + } +} diff --git a/packages/core/client/src/index.ts b/packages/core/client/src/index.ts index 1eab510b35..d2284473cd 100644 --- a/packages/core/client/src/index.ts +++ b/packages/core/client/src/index.ts @@ -63,7 +63,7 @@ export * from './variables'; export { withDynamicSchemaProps } from './hoc/withDynamicSchemaProps'; export { SchemaSettingsActionLinkItem } from './modules/actions/link/customizeLinkActionSettings'; -export { useURLAndParamsSchema } from './modules/actions/link/useURLAndParamsSchema'; +export { useURLAndHTMLSchema } from './modules/actions/link/useURLAndHTMLSchema'; export * from './modules/blocks/BlockSchemaToolbar'; export * from './modules/blocks/data-blocks/form'; export * from './modules/blocks/data-blocks/table'; diff --git a/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts b/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts index 592ab4f0c0..f679dbfaa0 100644 --- a/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts +++ b/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts @@ -8,7 +8,7 @@ */ import { expect, test } from '@nocobase/test/e2e'; -import { oneEmptyTableWithUsers } from './templates'; +import { oneEmptyTableWithUsers, openInNewWidow } from './templates'; test.describe('Link', () => { test('basic', async ({ page, mockPage, mockRecords }) => { @@ -84,4 +84,34 @@ test.describe('Link', () => { await expect(page.getByRole('button', { name: 'nocobase', exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: users[1].username, exact: true })).toBeVisible(); }); + + test('open in new window', async ({ page, mockPage, mockRecords }) => { + await mockPage(openInNewWidow).goto(); + const otherPage = mockPage(); + const otherPageUrl = await otherPage.getUrl(); + + // 默认情况下,点击链接按钮会在当前窗口打开 + await page.getByLabel('action-Action.Link-Link-').hover(); + await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:link-users').hover(); + await page.getByRole('menuitem', { name: 'Edit link' }).click(); + await page.getByLabel('block-item-users-table-URL').getByLabel('textbox').fill(otherPageUrl); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await page.getByLabel('action-Action.Link-Link-').click(); + expect(page.url().endsWith(otherPageUrl)).toBe(true); + + // 开启 “Open in new window” 选项后,点击链接按钮会在新窗口打开 + await page.goBack(); + await page.getByLabel('action-Action.Link-Link-').hover(); + await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:link-users').hover(); + await page.getByRole('menuitem', { name: 'Edit link' }).click(); + await page.getByLabel('Open in new window').check(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await page.getByLabel('action-Action.Link-Link-').click(); + await page.waitForTimeout(1000); + + const newPage = page.context().pages()[1]; + expect(newPage.url().endsWith(otherPageUrl)).toBe(true); + }); }); diff --git a/packages/core/client/src/modules/actions/__e2e__/link/templates.ts b/packages/core/client/src/modules/actions/__e2e__/link/templates.ts index 13885789fb..7994ed9c36 100644 --- a/packages/core/client/src/modules/actions/__e2e__/link/templates.ts +++ b/packages/core/client/src/modules/actions/__e2e__/link/templates.ts @@ -222,3 +222,173 @@ export const oneEmptyTableWithUsers = { 'x-index': 1, }, }; +export const openInNewWidow = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + 'x-app-version': '1.2.21-alpha', + properties: { + g3i4gd8j68w: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + 'x-app-version': '1.2.21-alpha', + properties: { + k0tx69u4ge5: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.21-alpha', + properties: { + hck9q8g9nfw: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.21-alpha', + properties: { + jq1vb4hd0vk: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.2.21-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.21-alpha', + 'x-uid': 'n7hoqcgj7nn', + 'x-async': false, + 'x-index': 1, + }, + '068dk7k35nk': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.2.21-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.2.21-alpha', + properties: { + mo7685lm8vz: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.2.21-alpha', + properties: { + t4s3iem6ael: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Link") }}', + 'x-action': 'customize:link', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:link', + 'x-component': 'Action.Link', + 'x-use-component-props': 'useLinkActionProps', + 'x-designer-props': { + linkageAction: true, + }, + 'x-uid': 'pvrerrk3t7w', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'cih8h4b2193', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '50yfcnetw5a', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4q78sr1a4so', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'k38ru1y7s6s', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'a0aiamh41on', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'gn2c5n843iv', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'gvbup8mmfql', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'af02n5ipxrk', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx b/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx index bef5ebacb5..5d23ba1386 100644 --- a/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx +++ b/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx @@ -17,15 +17,19 @@ import { SchemaSettings } from '../../../application/schema-settings/SchemaSetti import { useCollection_deprecated } from '../../../collection-manager'; import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; import { SchemaSettingsLinkageRules, SchemaSettingsModalItem } from '../../../schema-settings'; -import { useURLAndParamsSchema } from './useURLAndParamsSchema'; +import { useURLAndHTMLSchema } from './useURLAndHTMLSchema'; export function SchemaSettingsActionLinkItem() { const field = useField(); const fieldSchema = useFieldSchema(); const { dn } = useDesignable(); const { t } = useTranslation(); - const { urlSchema, paramsSchema } = useURLAndParamsSchema(); - const initialValues = { url: field.componentProps.url, params: field.componentProps.params || [{}] }; + const { urlSchema, paramsSchema, openInNewWindowSchema } = useURLAndHTMLSchema(); + const initialValues = { + url: field.componentProps.url, + params: field.componentProps.params || [{}], + openInNewWindow: field.componentProps.openInNewWindow, + }; return ( { + onSubmit={({ url, params, openInNewWindow }) => { const componentProps = fieldSchema['x-component-props'] || {}; componentProps.url = url; - fieldSchema['x-component-props'] = componentProps; - field.componentProps.url = url; componentProps.params = params; + componentProps.openInNewWindow = openInNewWindow; + fieldSchema['x-component-props'] = componentProps; + + field.componentProps.url = url; field.componentProps.params = params; + field.componentProps.openInNewWindow = openInNewWindow; + dn.emit('patch', { schema: { ['x-uid']: fieldSchema['x-uid'], diff --git a/packages/core/client/src/modules/actions/link/useURLAndParamsSchema.tsx b/packages/core/client/src/modules/actions/link/useURLAndHTMLSchema.tsx similarity index 73% rename from packages/core/client/src/modules/actions/link/useURLAndParamsSchema.tsx rename to packages/core/client/src/modules/actions/link/useURLAndHTMLSchema.tsx index 504f836091..87a081e202 100644 --- a/packages/core/client/src/modules/actions/link/useURLAndParamsSchema.tsx +++ b/packages/core/client/src/modules/actions/link/useURLAndHTMLSchema.tsx @@ -32,8 +32,7 @@ const getVariableComponentWithScope = (Com) => { }; }; -export const useURLAndParamsSchema = () => { - const fieldSchema = useFieldSchema(); +export const useURLAndHTMLSchema = () => { const { t } = useTranslation(); const Com = useMemo(() => getVariableComponentWithScope(Variable.TextArea), []); @@ -53,7 +52,40 @@ export const useURLAndParamsSchema = () => { }, }, }; - }, [t, fieldSchema]); + }, [t, Com]); + + const modeSchema = useMemo(() => { + return { + title: '{{t("Mode")}}', + 'x-component': 'Radio.Group', + 'x-decorator': 'FormItem', + default: 'url', + enum: [ + { value: 'url', label: t('URL') }, + { value: 'html', label: t('HTML') }, + ], + }; + }, [t]); + + const htmlSchema = useMemo(() => { + return { + title: t('html'), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': getVariableComponentWithScope(Variable.RawTextArea), + 'x-component-props': { + rows: 10, + }, + 'x-reactions': { + dependencies: ['mode'], + fulfill: { + state: { + hidden: '{{$deps[0] === "url"}}', + }, + }, + }, + }; + }, [t]); const paramsSchema = useMemo(() => { return { @@ -123,7 +155,24 @@ export const useURLAndParamsSchema = () => { }, }, }; - }, [fieldSchema]); + }, [Com]); - return { urlSchema, paramsSchema }; + const openInNewWindowSchema = useMemo(() => { + return { + type: 'boolean', + 'x-content': t('Open in new window'), + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-reactions': { + dependencies: ['mode'], + fulfill: { + state: { + hidden: '{{$deps[0] === "html"}}', + }, + }, + }, + }; + }, [t]); + + return { urlSchema, paramsSchema, openInNewWindowSchema, modeSchema, htmlSchema }; }; diff --git a/packages/core/client/src/modules/menu/LinkMenuItem.tsx b/packages/core/client/src/modules/menu/LinkMenuItem.tsx index 09b0c580c1..f236445d2d 100644 --- a/packages/core/client/src/modules/menu/LinkMenuItem.tsx +++ b/packages/core/client/src/modules/menu/LinkMenuItem.tsx @@ -16,7 +16,7 @@ import { SchemaInitializerItem, useSchemaInitializer } from '../../application'; import { useGlobalTheme } from '../../global-theme'; import { FormDialog, SchemaComponent, SchemaComponentOptions } from '../../schema-component'; import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers'; -import { useURLAndParamsSchema } from '../actions/link/useURLAndParamsSchema'; +import { useURLAndHTMLSchema } from '../actions/link/useURLAndHTMLSchema'; export const LinkMenuItem = () => { const { insert } = useSchemaInitializer(); @@ -24,7 +24,7 @@ export const LinkMenuItem = () => { const options = useContext(SchemaOptionsContext); const { theme } = useGlobalTheme(); const { styles } = useStyles(); - const { urlSchema, paramsSchema } = useURLAndParamsSchema(); + const { urlSchema, paramsSchema } = useURLAndHTMLSchema(); const handleClick = useCallback(async () => { const values = await FormDialog( diff --git a/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx b/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx index 3c621f4ce7..38cb63cf45 100644 --- a/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx @@ -22,7 +22,7 @@ import { SchemaSettingsSubMenu, useAPIClient, useDesignable, - useURLAndParamsSchema, + useURLAndHTMLSchema, } from '../../../'; const toItems = (properties = {}) => { @@ -61,7 +61,7 @@ const InsertMenuItems = (props) => { const { t } = useTranslation(); const { dn } = useDesignable(); const fieldSchema = useFieldSchema(); - const { urlSchema, paramsSchema } = useURLAndParamsSchema(); + const { urlSchema, paramsSchema } = useURLAndHTMLSchema(); const isSubMenu = fieldSchema['x-component'] === 'Menu.SubMenu'; if (!isSubMenu && insertPosition === 'beforeEnd') { return null; @@ -218,7 +218,7 @@ export const MenuDesigner = () => { const { t } = useTranslation(); const menuSchema = findMenuSchema(fieldSchema); const compile = useCompile(); - const { urlSchema, paramsSchema } = useURLAndParamsSchema(); + const { urlSchema, paramsSchema } = useURLAndHTMLSchema(); const onSelect = compile(menuSchema?.['x-component-props']?.['onSelect']); const items = toItems(menuSchema?.properties); const effects = (form) => { diff --git a/packages/core/utils/src/url.ts b/packages/core/utils/src/url.ts index f9fa037fa6..c7a8a671b0 100644 --- a/packages/core/utils/src/url.ts +++ b/packages/core/utils/src/url.ts @@ -7,8 +7,13 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +/** + * 是否是完整的 URL(带协议的) + * @param string + * @returns + */ export function isURL(string) { - let url; + let url: URL; try { url = new URL(string); diff --git a/packages/plugins/@nocobase/plugin-block-iframe/src/client/schemaSettings.tsx b/packages/plugins/@nocobase/plugin-block-iframe/src/client/schemaSettings.tsx index b23f45d3d6..34948241fc 100644 --- a/packages/plugins/@nocobase/plugin-block-iframe/src/client/schemaSettings.tsx +++ b/packages/plugins/@nocobase/plugin-block-iframe/src/client/schemaSettings.tsx @@ -17,7 +17,7 @@ import { useDesignable, useFormBlockContext, useRecord, - useURLAndParamsSchema, + useURLAndHTMLSchema, useVariableOptions, } from '@nocobase/client'; import React from 'react'; @@ -65,7 +65,7 @@ const commonOptions: any = { return data?.data; } }; - const { urlSchema, paramsSchema } = useURLAndParamsSchema(); + const { urlSchema, paramsSchema } = useURLAndHTMLSchema(); const submitHandler = async ({ mode, url, html, height, params }) => { const componentProps = fieldSchema['x-component-props'] || {}; componentProps['mode'] = mode; From 70c085532c2aa31831cf198ffea411c1d658c1da Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Fri, 19 Jul 2024 09:08:53 +0800 Subject: [PATCH 2/2] fix(variable): fix context error with 'current object' variable (#4901) * fix(variable): fix context error with 'current object' variable * test: add e2e test --- .../variable/__e2e__/currentObject.test.ts | 34 ++ .../src/modules/variable/__e2e__/templates.ts | 475 ++++++++++++++++++ .../antd/association-field/SubTable.tsx | 193 +++---- .../hooks/useIterationVariable.ts | 2 +- 4 files changed, 611 insertions(+), 93 deletions(-) create mode 100644 packages/core/client/src/modules/variable/__e2e__/currentObject.test.ts diff --git a/packages/core/client/src/modules/variable/__e2e__/currentObject.test.ts b/packages/core/client/src/modules/variable/__e2e__/currentObject.test.ts new file mode 100644 index 0000000000..ebab0daad8 --- /dev/null +++ b/packages/core/client/src/modules/variable/__e2e__/currentObject.test.ts @@ -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 { expect, test } from '@nocobase/test/e2e'; +import { T4874 } from './templates'; + +test.describe('variable: current object', () => { + test('in sub table', async ({ page, mockPage }) => { + await mockPage(T4874).goto(); + + // 在子表格中,使用“当前对象”变量 + await page.getByLabel('action-Action.Link-Edit-').click(); + await page.getByRole('button', { name: 'Role name', exact: true }).hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-roles' }) + .hover(); + await page.getByRole('menuitem', { name: 'Set default value' }).click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Role UID' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Add new' }).click(); + await page.getByRole('row', { name: 'table-index-4 block-item-' }).getByRole('textbox').nth(1).fill('123456'); + await expect(page.getByRole('row', { name: 'table-index-4 block-item-' }).getByRole('textbox').first()).toHaveValue( + '123456', + ); + }); +}); diff --git a/packages/core/client/src/modules/variable/__e2e__/templates.ts b/packages/core/client/src/modules/variable/__e2e__/templates.ts index a9176eec36..b756b8a1b2 100644 --- a/packages/core/client/src/modules/variable/__e2e__/templates.ts +++ b/packages/core/client/src/modules/variable/__e2e__/templates.ts @@ -388,3 +388,478 @@ export const APIToken = { 'x-index': 1, }, }; +export const T4874 = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + 'x-app-version': '1.2.21-alpha', + properties: { + d5btnu6z239: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + 'x-app-version': '1.2.21-alpha', + properties: { + dqmrwgx3h5j: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.21-alpha', + properties: { + '3q6wd3o9ve3': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.21-alpha', + properties: { + '1mw2svlh1t4': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.2.21-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.21-alpha', + 'x-uid': '66gj8p6hj5f', + 'x-async': false, + 'x-index': 1, + }, + no5z0g08zmh: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.2.21-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.2.21-alpha', + properties: { + '07oxu98m060': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.2.21-alpha', + properties: { + '40ond6gt5sp': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit") }}', + 'x-action': 'update', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:edit', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + icon: 'EditOutlined', + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Edit")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + properties: { + xv1rqhlertx: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.21-alpha', + properties: { + j7nzjmqyrt6: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.21-alpha', + properties: { + te5nblgjx91: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: false, + }, + 'x-acl-action': 'users:update', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': + 'useEditFormBlockDecoratorProps', + 'x-decorator-props': { + action: 'get', + dataSource: 'main', + collection: 'users', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:editForm', + 'x-component': 'CardItem', + 'x-app-version': '1.2.21-alpha', + properties: { + '1c7o18qoag1': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useEditFormBlockProps', + 'x-app-version': '1.2.21-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.2.21-alpha', + properties: { + '2k2z18gbdoy': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.21-alpha', + properties: { + c607566i7l3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.21-alpha', + properties: { + roles: { + 'x-uid': 'nz02ten5fzf', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'users.roles', + 'x-component-props': { + fieldNames: { + label: 'name', + value: 'name', + }, + mode: 'SubTable', + }, + 'x-app-version': '1.2.21-alpha', + properties: { + fj6qv4odhaa: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': + 'AssociationField.SubTable', + 'x-initializer': + 'table:configureColumns', + 'x-initializer-props': { + action: false, + }, + 'x-index': 1, + 'x-app-version': '1.2.21-alpha', + properties: { + '5j2fwrcfzuu': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': + 'TableV2.Column.Decorator', + 'x-toolbar': + 'TableColumnSchemaToolbar', + 'x-settings': + 'fieldSettings:TableColumn', + 'x-component': + 'TableV2.Column', + 'x-app-version': + '1.2.21-alpha', + properties: { + title: { + _isJSONSchemaObject: + true, + version: '2.0', + 'x-collection-field': + 'roles.title', + 'x-component': + 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-decorator': + 'FormItem', + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': + '1.2.21-alpha', + 'x-uid': '6hnbfgke4v9', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'xrfnjtprhgk', + 'x-async': false, + 'x-index': 1, + }, + nm5lqvmdmol: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': + 'TableV2.Column.Decorator', + 'x-toolbar': + 'TableColumnSchemaToolbar', + 'x-settings': + 'fieldSettings:TableColumn', + 'x-component': + 'TableV2.Column', + 'x-app-version': + '1.2.21-alpha', + properties: { + name: { + _isJSONSchemaObject: + true, + version: '2.0', + 'x-collection-field': + 'roles.name', + 'x-component': + 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-decorator': + 'FormItem', + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': + '1.2.21-alpha', + 'x-uid': 'nug7nu8ebhi', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '0esgluhnedb', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'xsx2rz70uwz', + 'x-async': false, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'r23ltwfb8by', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'tudt41ppj3p', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '96z12k7x884', + 'x-async': false, + 'x-index': 1, + }, + qq4upukd8xf: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'editForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.2.21-alpha', + 'x-uid': 'wdmwmy60e2r', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '33zsf8du7j2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'vfpz90ab0qw', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'gujrsvgrmjj', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '8xws8vfulhx', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'nl73uc805kr', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'izwg6n04xw3', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'p6wyw65zj1l', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '7hlmuhqrtpk', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'yfxjokc1rxx', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'rriritgfmdw', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'liz6v1lau34', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'vh37l43zevq', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'wxsf1stb1oi', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'nnkwbb57n9f', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'dfonxd85q4n', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'uxb09qu50d0', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'kg5tkkrw3x4', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx b/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx index 9ead589cfb..990723f68a 100644 --- a/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx @@ -14,7 +14,6 @@ import { observer, RecursionField, useFieldSchema } from '@formily/react'; import { action } from '@formily/reactive'; import { isArr } from '@formily/shared'; import { Button } from 'antd'; -import { unionBy, uniqBy } from 'lodash'; import React, { useContext, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -28,15 +27,66 @@ import { useCreateActionProps } from '../../../block-provider/hooks'; import { FormActiveFieldsProvider } from '../../../block-provider/hooks/useFormActiveFields'; import { TableSelectorParamsProvider } from '../../../block-provider/TableSelectorProvider'; import { CollectionProvider_deprecated } from '../../../collection-manager'; -import { CollectionRecordProvider, useCollectionRecord } from '../../../data-source'; +import { CollectionRecordProvider, useCollection, useCollectionRecord } from '../../../data-source'; +import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord'; import { FlagProvider } from '../../../flag-provider'; import { useCompile } from '../../hooks'; import { ActionContextProvider } from '../action'; import { Table } from '../table-v2/Table'; -import { useAssociationFieldContext, useFieldNames } from './hooks'; +import { SubFormProvider, useAssociationFieldContext, useFieldNames } from './hooks'; import { useTableSelectorProps } from './InternalPicker'; import { getLabelFormatValue, useLabelUiSchema } from './util'; -import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord'; + +const subTableContainer = css` + .ant-table-footer { + padding: 0 !important; + } + .ant-formily-item-error-help { + display: none; + } + .ant-description-textarea { + line-height: 34px; + } + .ant-table-cell .ant-formily-item-error-help { + display: block; + position: absolute; + font-size: 12px; + top: 100%; + background: #fff; + width: 100%; + margin-top: -15px; + padding: 3px; + z-index: 1; + border-radius: 3px; + box-shadow: 0 0 10px #eee; + animation: none; + transform: translateY(0); + opacity: 1; + } +`; + +const tableClassName = css` + .ant-formily-item.ant-formily-item-feedback-layout-loose { + margin-bottom: 0px !important; + } + .ant-formily-editable { + vertical-align: sub; + } + .ant-table-footer { + display: flex; + } +`; + +const addNewButtonClassName = css` + display: block; + border-radius: 0px; + border-right: 1px solid rgba(0, 0, 0, 0.06); +`; + +const selectButtonClassName = css` + display: block; + border-radius: 0px; +`; export const SubTable: any = observer( (props: any) => { @@ -50,6 +100,7 @@ export const SubTable: any = observer( const compile = useCompile(); const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label'); const recordV2 = useCollectionRecord(); + const collection = useCollection(); const move = (fromIndex: number, toIndex: number) => { if (toIndex === undefined) return; if (!isArr(field.value)) return; @@ -115,98 +166,56 @@ export const SubTable: any = observer( return filter; }; return ( -
+
- +
+ field.editable && ( + <> + {field.componentProps?.allowAddnew !== false && ( + + )} + {field.componentProps?.allowSelectExistingRecord && ( + + )} + + ) } - .ant-formily-editable { - vertical-align: sub; - } - .ant-table-footer { - display: flex; - } - `} - bordered - size={'small'} - field={field} - showIndex - dragSort={false} - showDel={field.editable} - pagination={false} - rowSelection={{ type: 'none', hideSelectAll: true }} - footer={() => - field.editable && ( - <> - {field.componentProps?.allowAddnew !== false && ( - - )} - {field.componentProps?.allowSelectExistingRecord && ( - - )} - - ) - } - isSubTable={true} - /> + isSubTable={true} + /> + diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts index 2bc7d1e567..62de51d6eb 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts @@ -10,11 +10,11 @@ import { Schema } from '@formily/json-schema'; import { useTranslation } from 'react-i18next'; import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; +import { useCollection } from '../../../data-source'; import { CollectionFieldOptions } from '../../../data-source/collection/Collection'; import { useFlag } from '../../../flag-provider'; import { useSubFormValue } from '../../../schema-component/antd/association-field/hooks'; import { useBaseVariable } from './useBaseVariable'; -import { useCollection } from '../../../data-source'; /** * @deprecated