From 881dced4dc381f16ccb0033ce8cdb56b135a3b6b Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Wed, 10 Jul 2024 12:23:45 +0800 Subject: [PATCH] feat(Menu): add support for setting search params and using variables in links (#4855) * refactor: extract urlSchema and paramsSchema to useURLAndParamsSchema * refactor: extract to useParseURLAndParams * feat: support to parse variables in Menu.URL * test: add e2e test * chore: fix e2e tests --- .../client/src/block-provider/hooks/index.ts | 36 +++-- packages/core/client/src/index.ts | 1 + .../actions/__e2e__/link/basic.test.ts | 2 +- .../link/customizeLinkActionSettings.tsx | 92 ++----------- .../actions/link/useURLAndParamsSchema.tsx | 129 ++++++++++++++++++ .../client/src/modules/menu/LinkMenuItem.tsx | 57 ++++---- .../menu/__e2e__/schemaInitializer.test.ts | 78 ++++++++--- .../antd/menu/Menu.Designer.tsx | 27 ++-- .../src/schema-component/antd/menu/Menu.tsx | 77 +++++++---- .../plugin-block-iframe/src/client/Iframe.tsx | 15 +- .../src/client/schemaSettings.tsx | 92 +------------ 11 files changed, 327 insertions(+), 279 deletions(-) create mode 100644 packages/core/client/src/modules/actions/link/useURLAndParamsSchema.tsx diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index a8d814f73..e599bb75c 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -1529,14 +1529,36 @@ export function appendQueryStringToUrl(url: string, queryString: string) { return url; } +export const useParseURLAndParams = () => { + const variables = useVariables(); + const localVariables = useLocalVariables(); + + const parseURLAndParams = useCallback( + async (url: string, params: { name: string; value: any }[]) => { + const queryString = await parseVariablesAndChangeParamsToQueryString({ + searchParams: params, + variables, + localVariables, + replaceVariableValue, + }); + const targetUrl = await replaceVariableValue(url, variables, localVariables); + const result = appendQueryStringToUrl(targetUrl, queryString); + + return result; + }, + [variables, localVariables], + ); + + return { parseURLAndParams }; +}; + export function useLinkActionProps() { const navigate = useNavigate(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); const url = fieldSchema?.['x-component-props']?.['url']; const searchParams = fieldSchema?.['x-component-props']?.['params'] || []; - const variables = useVariables(); - const localVariables = useLocalVariables(); + const { parseURLAndParams } = useParseURLAndParams(); return { type: 'default', @@ -1545,14 +1567,8 @@ export function useLinkActionProps() { message.warning(t('Please configure the URL')); return; } - const queryString = await parseVariablesAndChangeParamsToQueryString({ - searchParams, - variables, - localVariables, - replaceVariableValue, - }); - const targetUrl = await replaceVariableValue(url, variables, localVariables); - const link = appendQueryStringToUrl(targetUrl, queryString); + const link = await parseURLAndParams(url, searchParams); + if (link) { if (isURL(link)) { window.open(link, '_blank'); diff --git a/packages/core/client/src/index.ts b/packages/core/client/src/index.ts index 8483fdf9c..1eab510b3 100644 --- a/packages/core/client/src/index.ts +++ b/packages/core/client/src/index.ts @@ -63,6 +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 * 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 78fdb62e8..592ab4f0c 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 @@ -26,7 +26,7 @@ test.describe('Link', () => { await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:link-users' }).hover(); await page.getByRole('menuitem', { name: 'Edit link' }).click(); await page - .getByLabel('block-item-Variable.TextArea-users-table-URL') + .getByLabel('block-item-users-table-URL') .getByLabel('textbox') .fill(await nocoPage.getUrl()); await page.getByPlaceholder('Name').fill('id'); diff --git a/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx b/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx index bccbbbe7b..bef5ebacb 100644 --- a/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx +++ b/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx @@ -6,34 +6,27 @@ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { css } from '@emotion/css'; import { ArrayItems } from '@formily/antd-v5'; import { useField, useFieldSchema } from '@formily/react'; import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollectionRecord, useDesignable, useFormBlockContext, useRecord } from '../../../'; +import { useCollectionRecord, 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, SchemaSettingsModalItem } from '../../../schema-settings'; -import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks/useVariableOptions'; +import { useURLAndParamsSchema } from './useURLAndParamsSchema'; export function SchemaSettingsActionLinkItem() { const field = useField(); const fieldSchema = useFieldSchema(); const { dn } = useDesignable(); const { t } = useTranslation(); - const { form } = useFormBlockContext(); - const record = useRecord(); - const scope = useVariableOptions({ - collectionField: { uiSchema: fieldSchema }, - form, - record, - uiSchema: fieldSchema, - noDisabled: true, - }); + const { urlSchema, paramsSchema } = useURLAndParamsSchema(); + const initialValues = { url: field.componentProps.url, params: field.componentProps.params || [{}] }; + return ( .ant-space-item:first-child, - & > .ant-space-item:last-child { - flex-shrink: 0; - } - `, - }, - properties: { - name: { - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Input', - 'x-component-props': { - placeholder: `{{t("Name")}}`, - }, - }, - value: { - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Variable.TextArea', - 'x-component-props': { - scope, - placeholder: `{{t("Value")}}`, - useTypedConstant: true, - changeOnSelect: true, - }, - }, - remove: { - type: 'void', - 'x-decorator': 'FormItem', - 'x-component': 'ArrayItems.Remove', - }, - }, - }, - }, - }, - properties: { - add: { - type: 'void', - title: `{{t("Add parameter")}}`, - 'x-component': 'ArrayItems.Addition', - }, - }, + ...urlSchema, + required: true, }, + params: paramsSchema, }, }} onSubmit={({ url, params }) => { @@ -133,6 +58,7 @@ export function SchemaSettingsActionLinkItem() { }); dn.refresh(); }} + initialValues={initialValues} /> ); } diff --git a/packages/core/client/src/modules/actions/link/useURLAndParamsSchema.tsx b/packages/core/client/src/modules/actions/link/useURLAndParamsSchema.tsx new file mode 100644 index 000000000..504f83609 --- /dev/null +++ b/packages/core/client/src/modules/actions/link/useURLAndParamsSchema.tsx @@ -0,0 +1,129 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { css } from '@emotion/css'; +import { useFieldSchema } from '@formily/react'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; +import { useRecord } from '../../../record-provider'; +import { Variable } from '../../../schema-component/antd/variable/Variable'; +import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks/useVariableOptions'; + +const getVariableComponentWithScope = (Com) => { + return (props) => { + const fieldSchema = useFieldSchema(); + const { form } = useFormBlockContext(); + const record = useRecord(); + const scope = useVariableOptions({ + collectionField: { uiSchema: fieldSchema }, + form, + record, + uiSchema: fieldSchema, + noDisabled: true, + }); + return ; + }; +}; + +export const useURLAndParamsSchema = () => { + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const Com = useMemo(() => getVariableComponentWithScope(Variable.TextArea), []); + + const urlSchema = useMemo(() => { + return { + title: t('URL'), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': Com, + description: t('Do not concatenate search params in the URL'), + 'x-reactions': { + dependencies: ['mode'], + fulfill: { + state: { + hidden: '{{$deps[0] === "html"}}', + }, + }, + }, + }; + }, [t, fieldSchema]); + + const paramsSchema = useMemo(() => { + return { + type: 'array', + 'x-component': 'ArrayItems', + 'x-decorator': 'FormItem', + title: `{{t("Search parameters")}}`, + items: { + type: 'object', + properties: { + space: { + type: 'void', + 'x-component': 'Space', + 'x-component-props': { + style: { + flexWrap: 'nowrap', + maxWidth: '100%', + }, + className: css` + & > .ant-space-item:first-child, + & > .ant-space-item:last-child { + flex-shrink: 0; + } + `, + }, + properties: { + name: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-component-props': { + placeholder: `{{t("Name")}}`, + }, + }, + value: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': Com, + 'x-component-props': { + placeholder: `{{t("Value")}}`, + useTypedConstant: true, + changeOnSelect: true, + }, + }, + remove: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.Remove', + }, + }, + }, + }, + }, + 'x-reactions': { + dependencies: ['mode'], + fulfill: { + state: { + hidden: '{{$deps[0] === "html"}}', + }, + }, + }, + properties: { + add: { + type: 'void', + title: `{{t("Add parameter")}}`, + 'x-component': 'ArrayItems.Addition', + }, + }, + }; + }, [fieldSchema]); + + return { urlSchema, paramsSchema }; +}; diff --git a/packages/core/client/src/modules/menu/LinkMenuItem.tsx b/packages/core/client/src/modules/menu/LinkMenuItem.tsx index 9dbbf56f8..09b0c580c 100644 --- a/packages/core/client/src/modules/menu/LinkMenuItem.tsx +++ b/packages/core/client/src/modules/menu/LinkMenuItem.tsx @@ -11,10 +11,12 @@ import { FormLayout } from '@formily/antd-v5'; import { SchemaOptionsContext } from '@formily/react'; import React, { useCallback, useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import { FormDialog, SchemaComponent, SchemaComponentOptions } from '../../schema-component'; +import { Router } from 'react-router-dom'; 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'; export const LinkMenuItem = () => { const { insert } = useSchemaInitializer(); @@ -22,45 +24,45 @@ export const LinkMenuItem = () => { const options = useContext(SchemaOptionsContext); const { theme } = useGlobalTheme(); const { styles } = useStyles(); + const { urlSchema, paramsSchema } = useURLAndParamsSchema(); const handleClick = useCallback(async () => { const values = await FormDialog( t('Add link'), () => { return ( - - - + + + - - + }} + /> + + + ); }, theme, ).open({ initialValues: {}, }); - const { title, href, icon } = values; + const { title, href, params, icon } = values; insert({ type: 'void', title, @@ -69,6 +71,7 @@ export const LinkMenuItem = () => { 'x-component-props': { icon, href, + params, }, 'x-server-hooks': [ { diff --git a/packages/core/client/src/modules/menu/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/menu/__e2e__/schemaInitializer.test.ts index 2cf1f6819..8da21efcf 100644 --- a/packages/core/client/src/modules/menu/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/menu/__e2e__/schemaInitializer.test.ts @@ -7,71 +7,107 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { uid } from '@formily/shared'; import { expect, groupPageEmpty, test } from '@nocobase/test/e2e'; test.describe('add menu item', () => { test('header', async ({ page, deletePage }) => { await page.goto('/'); + const pageGroup = uid(); + const pageItem = uid(); + const pageLink = uid(); // add group await page.getByTestId('schema-initializer-Menu-header').hover(); await page.getByRole('menuitem', { name: 'Group' }).click(); await page.getByRole('textbox').click(); - await page.getByRole('textbox').fill('page group'); + await page.getByRole('textbox').fill(pageGroup); await page.getByRole('button', { name: 'OK', exact: true }).click(); - await expect(page.getByLabel('page group', { exact: true })).toBeVisible(); + await expect(page.getByLabel(pageGroup, { exact: true })).toBeVisible(); // add page await page.getByTestId('schema-initializer-Menu-header').hover(); await page.getByRole('menuitem', { name: 'Page', exact: true }).click(); await page.getByRole('textbox').click(); - await page.getByRole('textbox').fill('page item'); + await page.getByRole('textbox').fill(pageItem); await page.getByRole('button', { name: 'OK', exact: true }).click(); - await expect(page.getByLabel('page item', { exact: true })).toBeVisible(); + await expect(page.getByLabel(pageItem, { exact: true })).toBeVisible(); // add link await page.getByTestId('schema-initializer-Menu-header').hover(); await page.getByRole('menuitem', { name: 'Link', exact: true }).click(); await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').click(); - await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').fill('page link'); - await page.getByLabel('block-item-Input-Link').getByRole('textbox').click(); - await page.getByLabel('block-item-Input-Link').getByRole('textbox').fill('baidu.com'); + await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').fill(pageLink); + await page.getByLabel('block-item-URL').getByLabel('textbox').fill('baidu.com'); await page.getByRole('button', { name: 'OK', exact: true }).click(); - await expect(page.getByText('page link', { exact: true })).toBeVisible(); + await expect(page.getByText(pageLink, { exact: true })).toBeVisible(); // delete pages - await deletePage('page group'); - await deletePage('page item'); - await deletePage('page link'); + await deletePage(pageGroup); + await deletePage(pageItem); + await deletePage(pageLink); }); test('sidebar', async ({ page, mockPage }) => { await mockPage(groupPageEmpty).goto(); + const pageGroupSide = uid(); + const pageItemSide = uid(); + const pageLinkSide = uid(); + // add group in side await page.getByTestId('schema-initializer-Menu-side').hover(); await page.getByRole('menuitem', { name: 'Group', exact: true }).click(); - await page.getByRole('textbox').click(); - await page.getByRole('textbox').fill('page group side'); + await page.getByRole('textbox').fill(pageGroupSide); await page.getByRole('button', { name: 'OK', exact: true }).click(); - await expect(page.getByText('page group side', { exact: true })).toBeVisible(); + await expect(page.getByText(pageGroupSide, { exact: true })).toBeVisible(); // add page in side await page.getByTestId('schema-initializer-Menu-side').hover(); await page.getByRole('menuitem', { name: 'Page', exact: true }).click(); - await page.getByRole('textbox').click(); - await page.getByRole('textbox').fill('page item side'); + await page.getByRole('textbox').fill(pageItemSide); await page.getByRole('button', { name: 'OK', exact: true }).click(); - await expect(page.getByText('page item side', { exact: true })).toBeVisible(); + await expect(page.getByText(pageItemSide, { exact: true })).toBeVisible(); // add link in side await page.getByTestId('schema-initializer-Menu-side').hover(); await page.getByRole('menuitem', { name: 'Link', exact: true }).click(); await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').click(); - await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').fill('link item side'); - await page.getByLabel('block-item-Input-Link').getByRole('textbox').click(); - await page.getByLabel('block-item-Input-Link').getByRole('textbox').fill('/'); + await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').fill(pageLinkSide); + await page.getByLabel('block-item-URL').getByLabel('textbox').fill('/'); await page.getByRole('button', { name: 'OK', exact: true }).click(); - await expect(page.getByText('link item side', { exact: true })).toBeVisible(); + await expect(page.getByText(pageLinkSide, { exact: true })).toBeVisible(); + }); + + test('link: use variable', async ({ page, deletePage }) => { + await page.goto('/'); + const pageLink = uid(); + + const token = await page.evaluate(() => { + return window.localStorage.getItem('NOCOBASE_TOKEN'); + }); + + // add link + await page.getByTestId('schema-initializer-Menu-header').hover(); + await page.getByRole('menuitem', { name: 'Link', exact: true }).click(); + await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').click(); + await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').fill(pageLink); + await page.getByLabel('block-item-URL').getByLabel('textbox').fill('https://www.baidu.com'); + await page.getByRole('button', { name: 'plus Add parameter' }).click(); + await page.getByPlaceholder('Name').click(); + await page.getByPlaceholder('Name').fill('token'); + await page.getByLabel('block-item-ArrayItems-Search').getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'API token' }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + + // open link page + await page.getByLabel(pageLink).click(); + await page.waitForTimeout(1000); + + // After clicking, it will redirect to another page, so we need to get the instance of the new page + const newPage = page.context().pages()[1]; + expect(newPage.url()).toBe('https://www.baidu.com/?token=' + token); + + await deletePage(pageLink); }); }); 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 a67d45d06..3c621f4ce 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,6 +22,7 @@ import { SchemaSettingsSubMenu, useAPIClient, useDesignable, + useURLAndParamsSchema, } from '../../../'; const toItems = (properties = {}) => { @@ -60,6 +61,7 @@ const InsertMenuItems = (props) => { const { t } = useTranslation(); const { dn } = useDesignable(); const fieldSchema = useFieldSchema(); + const { urlSchema, paramsSchema } = useURLAndParamsSchema(); const isSubMenu = fieldSchema['x-component'] === 'Menu.SubMenu'; if (!isSubMenu && insertPosition === 'beforeEnd') { return null; @@ -184,15 +186,12 @@ const InsertMenuItems = (props) => { 'x-component': 'IconPicker', 'x-decorator': 'FormItem', }, - href: { - title: t('Link'), - 'x-component': 'Input', - 'x-decorator': 'FormItem', - }, + href: urlSchema, + params: paramsSchema, }, } as ISchema } - onSubmit={({ title, icon, href }) => { + onSubmit={({ title, icon, href, params }) => { dn.insertAdjacent(insertPosition, { type: 'void', title, @@ -201,6 +200,7 @@ const InsertMenuItems = (props) => { 'x-component-props': { icon, href, + params, }, 'x-server-hooks': serverHooks, }); @@ -218,6 +218,7 @@ export const MenuDesigner = () => { const { t } = useTranslation(); const menuSchema = findMenuSchema(fieldSchema); const compile = useCompile(); + const { urlSchema, paramsSchema } = useURLAndParamsSchema(); const onSelect = compile(menuSchema?.['x-component-props']?.['onSelect']); const items = toItems(menuSchema?.properties); const effects = (form) => { @@ -261,12 +262,10 @@ export const MenuDesigner = () => { icon: field.componentProps.icon, }; if (fieldSchema['x-component'] === 'Menu.URL') { - schema.properties['href'] = { - title: t('Link'), - 'x-component': 'Input', - 'x-decorator': 'FormItem', - }; + schema.properties['href'] = urlSchema; + schema.properties['params'] = paramsSchema; initialValues['href'] = field.componentProps.href; + initialValues['params'] = field.componentProps.params; } return ( @@ -275,7 +274,7 @@ export const MenuDesigner = () => { eventKey="edit" schema={schema as ISchema} initialValues={initialValues} - onSubmit={({ title, icon, href }) => { + onSubmit={({ title, icon, href, params }) => { const schema = { ['x-uid']: fieldSchema['x-uid'], 'x-server-hooks': [ @@ -293,10 +292,12 @@ export const MenuDesigner = () => { } field.componentProps.icon = icon; field.componentProps.href = href; - schema['x-component-props'] = { icon, href }; + field.componentProps.params = params; + schema['x-component-props'] = { icon, href, params }; fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; fieldSchema['x-component-props']['icon'] = icon; fieldSchema['x-component-props']['href'] = href; + fieldSchema['x-component-props']['params'] = params; onSelect?.({ item: { props: { schema: fieldSchema } } }); dn.emit('patch', { schema, diff --git a/packages/core/client/src/schema-component/antd/menu/Menu.tsx b/packages/core/client/src/schema-component/antd/menu/Menu.tsx index 67bd23b2c..9a337c861 100644 --- a/packages/core/client/src/schema-component/antd/menu/Menu.tsx +++ b/packages/core/client/src/schema-component/antd/menu/Menu.tsx @@ -24,7 +24,7 @@ import React, { createContext, useContext, useEffect, useMemo, useRef, useState import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { createDesignable, DndContext, SortableItem, useDesignable, useDesigner } from '../..'; -import { Icon, useAPIClient, useSchemaInitializerRender } from '../../../'; +import { Icon, useAPIClient, useParseURLAndParams, useSchemaInitializerRender } from '../../../'; import { useCollectMenuItems, useMenuItem } from '../../../hooks/useMenuItem'; import { useProps } from '../../hooks/useProps'; import { useMenuTranslation } from './locale'; @@ -551,9 +551,55 @@ Menu.Item = observer( { displayName: 'Menu.Item' }, ); +const MenuURLButton = ({ href, params, icon }) => { + const field = useField(); + const { t } = useMenuTranslation(); + const Designer = useContext(MenuItemDesignerContext); + const { parseURLAndParams } = useParseURLAndParams(); + const urlRef = useRef(href); + + useEffect(() => { + const run = async () => { + try { + urlRef.current = await parseURLAndParams(href, params || []); + } catch (err) { + console.error(err); + urlRef.current = href; + } + }; + run(); + }, [href, JSON.stringify(params), parseURLAndParams]); + + return ( + { + window.open(urlRef.current, '_blank'); + event.preventDefault(); + event.stopPropagation(); + }} + removeParentsIfNoChildren={false} + aria-label={t(field.title)} + > + + + {t(field.title)} + + + + ); +}; + Menu.URL = observer( (props) => { - const { t } = useMenuTranslation(); const { pushMenuItem } = useCollectMenuItems(); const { icon, children, ...others } = props; const schema = useFieldSchema(); @@ -576,35 +622,12 @@ Menu.URL = observer( label: ( - { - window.open(props.href, '_blank'); - event.preventDefault(); - event.stopPropagation(); - }} - removeParentsIfNoChildren={false} - aria-label={t(field.title)} - > - - - {t(field.title)} - - - + ), }; - }, [field.title, icon, props.href, schema, Designer]); + }, [field.title, icon, props.href, schema, JSON.stringify(props.params)]); pushMenuItem(item); return null; diff --git a/packages/plugins/@nocobase/plugin-block-iframe/src/client/Iframe.tsx b/packages/plugins/@nocobase/plugin-block-iframe/src/client/Iframe.tsx index 49db34621..5a3e13992 100644 --- a/packages/plugins/@nocobase/plugin-block-iframe/src/client/Iframe.tsx +++ b/packages/plugins/@nocobase/plugin-block-iframe/src/client/Iframe.tsx @@ -9,11 +9,10 @@ import { observer, useField } from '@formily/react'; import { - appendQueryStringToUrl, - parseVariablesAndChangeParamsToQueryString, replaceVariableValue, useBlockHeight, useLocalVariables, + useParseURLAndParams, useRequest, useVariables, } from '@nocobase/client'; @@ -49,7 +48,7 @@ export const Iframe: any = observer( ready: mode === 'html' && !!htmlId, }, ); - + const { parseURLAndParams } = useParseURLAndParams(); const [src, setSrc] = useState(null); useEffect(() => { @@ -61,15 +60,7 @@ export const Iframe: any = observer( setSrc(dataUrl); } else { try { - const tempUrl = await replaceVariableValue(url, variables, localVariables); - const queryString = await parseVariablesAndChangeParamsToQueryString({ - searchParams: params || [], - variables, - localVariables, - replaceVariableValue, - }); - - const targetUrl = appendQueryStringToUrl(tempUrl, queryString); + const targetUrl = parseURLAndParams(url, params || []); setSrc(targetUrl); } catch (error) { console.error('Error fetching target URL:', error); 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 8619c0eba..b23f45d3d 100644 --- a/packages/plugins/@nocobase/plugin-block-iframe/src/client/schemaSettings.tsx +++ b/packages/plugins/@nocobase/plugin-block-iframe/src/client/schemaSettings.tsx @@ -7,17 +7,17 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { css } from '@emotion/css'; import { ISchema, useField, useFieldSchema } from '@formily/react'; import { uid } from '@formily/shared'; import { SchemaSettings, + SchemaSettingsBlockHeightItem, Variable, useAPIClient, useDesignable, - SchemaSettingsBlockHeightItem, useFormBlockContext, useRecord, + useURLAndParamsSchema, useVariableOptions, } from '@nocobase/client'; import React from 'react'; @@ -50,7 +50,7 @@ const commonOptions: any = { const { t } = useTranslation(); const { dn } = useDesignable(); const api = useAPIClient(); - const { mode, url, htmlId, height = '60vh' } = fieldSchema['x-component-props'] || {}; + const { mode, url, params, htmlId, height = '60vh' } = fieldSchema['x-component-props'] || {}; const saveHtml = async (html: string) => { const options = { values: { html }, @@ -65,7 +65,7 @@ const commonOptions: any = { return data?.data; } }; - + const { urlSchema, paramsSchema } = useURLAndParamsSchema(); const submitHandler = async ({ mode, url, html, height, params }) => { const componentProps = fieldSchema['x-component-props'] || {}; componentProps['mode'] = mode; @@ -94,6 +94,7 @@ const commonOptions: any = { mode, url, height, + params, }; if (htmlId) { // eslint-disable-next-line no-unsafe-optional-chaining @@ -118,89 +119,10 @@ const commonOptions: any = { ], }, url: { - title: t('URL'), - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': getVariableComponentWithScope(Variable.TextArea), - description: t('Do not concatenate search params in the URL'), + ...urlSchema, required: true, - 'x-reactions': { - dependencies: ['mode'], - fulfill: { - state: { - hidden: '{{$deps[0] === "html"}}', - }, - }, - }, - }, - params: { - type: 'array', - 'x-component': 'ArrayItems', - 'x-decorator': 'FormItem', - title: `{{t("Search parameters")}}`, - default: fieldSchema?.['x-component-props']?.params || [{}], - items: { - type: 'object', - properties: { - space: { - type: 'void', - 'x-component': 'Space', - 'x-component-props': { - style: { - flexWrap: 'nowrap', - maxWidth: '100%', - }, - className: css` - & > .ant-space-item:first-child, - & > .ant-space-item:last-child { - flex-shrink: 0; - } - `, - }, - properties: { - name: { - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Input', - 'x-component-props': { - placeholder: `{{t("Name")}}`, - }, - }, - value: { - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': getVariableComponentWithScope(Variable.TextArea), - 'x-component-props': { - placeholder: `{{t("Value")}}`, - useTypedConstant: true, - changeOnSelect: true, - }, - }, - remove: { - type: 'void', - 'x-decorator': 'FormItem', - 'x-component': 'ArrayItems.Remove', - }, - }, - }, - }, - }, - 'x-reactions': { - dependencies: ['mode'], - fulfill: { - state: { - hidden: '{{$deps[0] === "html"}}', - }, - }, - }, - properties: { - add: { - type: 'void', - title: `{{t("Add parameter")}}`, - 'x-component': 'ArrayItems.Addition', - }, - }, }, + params: paramsSchema, html: { title: t('html'), type: 'string',