Feature/1.4 (#154)

- Load balancing (can connect to multiple accounts, automatically switch accounts when there is no quota)
- AI call log
- Model rate configuration
This commit is contained in:
ScarChin
2025-01-07 18:47:08 +08:00
committed by GitHub
parent 09b2a7f1a4
commit d7e28c9704
300 changed files with 29714 additions and 23492 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
/config.yml
/build/
/apipark
.gitlab-ci.yml
.gitlab-ci.yml
-9
View File
@@ -1,9 +0,0 @@
{
"cSpell.words": [
"Antd",
"apinto",
"Apipark",
"logsettings",
"resourcesettings"
]
}
+7
View File
@@ -0,0 +1,7 @@
node_modules
dist
build
coverage
.next
*.d.ts
*.js
+7 -2
View File
@@ -18,13 +18,18 @@
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["react", "@typescript-eslint", "prettier"],
"plugins": ["react", "@typescript-eslint", "prettier", "unused-imports"],
"rules": {
"react/react-in-jsx-scope": "off",
"prettier/prettier": "error",
"@typescript-eslint/no-explicit-any": "warn",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["warn"]
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{ "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" }
]
},
"settings": {
"react": {
+7 -6
View File
@@ -1,10 +1,11 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 120,
"useTabs": false,
"tabWidth": 2,
"printWidth": 100,
"semi": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"jsxSingleQuote": false
"arrowParens": "always",
"trailingComma": "none",
"singleQuote": true,
"bracketLine": true
}
+45
View File
@@ -0,0 +1,45 @@
Create detailed components with these requirements:
1. Use 'use client' directive for client-side components
2. Style with Tailwind CSS utility classes for responsive design
3. Use React Router for navigation
4. Use Ant Design for UI components
5. Use iconify React for icons (from @iconify/react package). Do NOT use other UI libraries unless requested
6. Use local photos from public folder where appropriate, only valid URLs you know exist
7. Create root layout.tsx page that wraps necessary navigation items to all pages
8. MUST implement the navigation elements items in their rightful place i.e. Left sidebar, Top header
9. Accurately implement necessary grid layouts
10. Follow proper import practices:
- Use @/ path aliases
- Keep component imports organized
- Update current packages/core/src/pages/Root.tsx with new comprehensive code
- Don't forget root route (page.tsx) handling
- You MUST complete the entire prompt before stopping
11. Table component should use `import PageList from "@common/components/aoplatform/PageList.tsx"`
12. PageList component MUST use addNewBtnTitle for add button, NOT toolBarRender. Example:
<PageList
id="global_team"
className="pl-btnbase"
ref={pageListRef}
columns = {[...columns]}
request = {()=>getTeamList()}
showPagination={false}
addNewBtnTitle={$t('添加团队')}
addNewBtnAccess = "system.organization.team.add"
searchPlaceholder={$t("输入名称、ID、负责人查找团队")}
onAddNewBtnClick={()=>{openModal('add')}}
onSearchWordChange={(e)=>{setSearchWord(e.target.value)}}
onRowClick={(row:TeamTableListItem)=>(navigate(`../inside/${row.id}/setting`))}
/>
13. use `const { fetchData } = useFetch()` to fetch http data,such as
```tsx
fetchData<BasicResponse<{ profile: UserInfoType }>>('account/profile', { method: 'GET' }).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setUserInfo(data.profile)
dispatch({ type: 'UPDATE_USERDATA', userData: data.profile })
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
```
14. can't not import new package!
+14 -18
View File
@@ -6,7 +6,7 @@ const systemLanguage = {
en_US: 'en-US',
zh_CN: 'zh-CN',
ja_JP: 'ja-JP',
zh_TW: 'zh-TW'
zh_TW: 'zh-TW',
};
const localesDir = 'packages/common/src/locales/scan';
const newJsonDir = 'packages/common/src/locales/scan/newJson';
@@ -19,7 +19,7 @@ fs.readdirSync(localesDir).forEach(file => {
const lang = path.basename(file, '.json');
const filePath = path.join(localesDir, file);
try {
console.log('Current working directory:', process.cwd(),filePath);
console.log('Current working directory:', process.cwd(), filePath);
const existJsonData = fs.readFileSync(filePath);
existData[lang] = JSON.parse(existJsonData);
} catch (error) {
@@ -36,20 +36,18 @@ fs.readdirSync(localesDir).forEach(file => {
const keyList = Object.keys(existData);
// 清空 newJson 目录下的所有语言文件
Object.values(systemLanguage).forEach(lng => {
const newJsonPath = path.join(newJsonDir, `${lng}.json`);
fs.writeFileSync(newJsonPath, JSON.stringify({})); // 清空文件
fs.writeFileSync(newJsonPath, JSON.stringify({})); // 清空文件
});
module.exports = {
input: [
'packages/*/src/**/*.{js,jsx,tsx,ts}',
// 不需要扫描的文件加!
'!packages/*/src/locales/**',
'!**/node_modules/**'
'!**/node_modules/**',
],
output: 'packages/common/src/locales/scan', // 输出目录
options: {
@@ -62,15 +60,15 @@ module.exports = {
loadPath: './newJson/{{lng}}.json', // 输入路径 (手动新建目录)
savePath: './newJson/{{lng}}.json', // 输出路径 (输出会根据输入路径内容自增, 不会覆盖已有的key)
jsonIndent: 2,
lineEnding: '\n'
lineEnding: '\n',
},
removeUnusedKeys: true,
nsSeparator: false, // namespace separator
keySeparator: false, // key separator
interpolation: {
prefix: '{{',
suffix: '}}'
}
suffix: '}}',
},
},
// 这里我们要实现将中文转换成crc格式, 通过crc格式key作为索引, 最终实现语言包的切换.
transform: function (file, enc, done) {
@@ -80,11 +78,10 @@ module.exports = {
parser.parseFuncFromString(content, { list: ['t'] }, (key, options) => {
options.defaultValue = key;
const hashKey = `K${crc32(key).toString(16)}`; // crc32转换格式
keyHashMap[key] = hashKey;
keyHashMap[key] = hashKey;
// 遍历每种语言,逐个语言检查翻译是否存在
keyList.forEach((lng) => {
keyList.forEach(lng => {
const langData = existData[lng] || {};
// 如果某语言没有翻译该字段,则记录到该语言的 newJson 文件中
@@ -116,13 +113,12 @@ module.exports = {
});
done();
},
flush: function(done) {
flush: function (done) {
// 将 keyHashMap 写入文件
fs.writeFileSync(keyHashFile, JSON.stringify(keyHashMap, null, 2));
// 遍历每种语言,处理旧字段
keyList.forEach((lng) => {
keyList.forEach(lng => {
const localeFilePath = path.join(localesDir, `${lng}.json`);
const oldJsonPath = path.join(oldJsonDir, `${lng}.json`);
const langData = existData[lng] || {};
@@ -132,7 +128,7 @@ module.exports = {
// 将不存在于 keyHashMap 中的键移动到 oldJson 文件中
Object.keys(langData).forEach(hashKey => {
if (!Object.values(keyHashMap).includes(hashKey)) {
oldJsonData[hashKey] = langData[hashKey]; // 将旧的 key 移到 oldJson 中
oldJsonData[hashKey] = langData[hashKey]; // 将旧的 key 移到 oldJson 中
}
});
@@ -142,5 +138,5 @@ module.exports = {
}
});
done();
}
};
},
};
+8 -1
View File
@@ -13,7 +13,9 @@
"serve:remotes": "lerna run serve --scope=remote --parallel",
"dev": "lerna run dev --scope=core --stream",
"stop": "kill-port --port 5000",
"scan": "i18next-scanner --config i18next-scanner.config.js"
"scan": "i18next-scanner --config i18next-scanner.config.js",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix && prettier --write ."
},
"keywords": [],
"author": "",
@@ -65,8 +67,12 @@
"antd": "^5.19.4",
"babel-jest": "^29.7.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-react": "7.37.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"eslint-plugin-unused-imports": "^4.1.4",
"file-saver": "^2.0.5",
"i18next-scanner": "^4.5.0",
"jest": "^29.7.0",
@@ -78,6 +84,7 @@
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"postcss-nested": "^6.0.1",
"prettier": "^3.1.1",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.1.2",
"typescript": "^5.2.2",
@@ -1,39 +1,37 @@
import React from "react"
import SwaggerUI from 'swagger-ui-react';
import 'swagger-ui-react/swagger-ui.css';
import React from 'react'
import SwaggerUI from 'swagger-ui-react'
import 'swagger-ui-react/swagger-ui.css'
export default function ApiDocument({spec}:{spec?:string|object}) {
class OperationsLayout extends React.Component {
render() {
const {
getComponent
} = this.props
const Operations = getComponent("operations", true)
return (
<div className="swagger-ui">
<Operations />
</div>
)
}
}
// Create the plugin that provides our layout component
const OperationsLayoutPlugin = () => {
return {
components: {
OperationsLayout: OperationsLayout
}
}
export default function ApiDocument({ spec }: { spec?: string | object }) {
class OperationsLayout extends React.Component {
render() {
const { getComponent } = this.props
const Operations = getComponent('operations', true)
return (
<div className="swagger-ui">
<Operations />
</div>
)
}
return(
<SwaggerUI
spec={spec}
supportedSubmitMethods={[]}
customComponents={{Header:()=>null}}
layout="OperationsLayout"
plugins={[OperationsLayoutPlugin ]} />
)
}
}
// Create the plugin that provides our layout component
const OperationsLayoutPlugin = () => {
return {
components: {
OperationsLayout: OperationsLayout
}
}
}
return (
<SwaggerUI
spec={spec}
supportedSubmitMethods={[]}
customComponents={{ Header: () => null }}
layout="OperationsLayout"
plugins={[OperationsLayoutPlugin]}
/>
)
}
@@ -1,29 +1,22 @@
import {
MenuProps,
App,
Button,
ConfigProvider,
Dropdown
} from 'antd';
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import Logo from '@common/assets/layout-logo.png';
import { ProConfigProvider, ProLayout } from '@ant-design/pro-components'
import AvatarPic from '@common/assets/default-avatar.png'
import { useCallback, useEffect, useMemo, useState} from "react";
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx';
import { PERMISSION_DEFINITION } from '@common/const/permissions.ts';
import { BasicResponse, RESPONSE_TIPS, routerKeyMap, STATUS_CODE } from '@common/const/const.tsx';
import { UserInfoType } from '@common/const/type.ts';
import { useFetch } from '@common/hooks/http.ts';
import { ProjectFilled } from '@ant-design/icons';
import { getNavItem, transformMenuData } from '@common/utils/navigation';
import { Icon } from '@iconify/react';
import { $t } from '@common/locales';
import { ProConfigProvider, ProLayout } from '@ant-design/pro-components';
import LanguageSetting from './LanguageSetting';
import { usePluginSlotHub } from '@common/contexts/PluginSlotHubContext';
import Logo from '@common/assets/layout-logo.png'
import { BasicResponse, RESPONSE_TIPS, routerKeyMap, STATUS_CODE } from '@common/const/const.tsx'
import { PERMISSION_DEFINITION } from '@common/const/permissions.ts'
import { UserInfoType } from '@common/const/type.ts'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import { usePluginSlotHub } from '@common/contexts/PluginSlotHubContext'
import { useFetch } from '@common/hooks/http.ts'
import { $t } from '@common/locales'
import { transformMenuData } from '@common/utils/navigation'
import { Icon } from '@iconify/react'
import { App, Button, ConfigProvider, Dropdown, MenuProps } from 'antd'
import { useEffect, useMemo, useState } from 'react'
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import LanguageSetting from './LanguageSetting'
const APP_MODE = import.meta.env.VITE_APP_MODE;
export type MenuItem = Required<MenuProps>['items'][number];
const APP_MODE = import.meta.env.VITE_APP_MODE
export type MenuItem = Required<MenuProps>['items'][number]
const themeToken = {
bgLayout: '#17163E;',
@@ -32,92 +25,99 @@ const themeToken = {
},
pageContainer: {
paddingBlockPageContainerContent: 0,
paddingInlinePageContainerContent: 0,
paddingInlinePageContainerContent: 0
}
}
function BasicLayout({ project = 'core' }: { project: string }) {
const navigator = useNavigate()
const location = useLocation()
const currentUrl = location.pathname
const { state, accessData, checkPermission, accessInit, dispatch, resetAccess, getGlobalAccessData, menuList } =
useGlobalContext()
const [pathname, setPathname] = useState(currentUrl)
const mainPage = project === 'core' ? '/service/list' : '/serviceHub/list'
const [menuItems, setMenuItems] = useState<MenuProps['items']>()
const pluginSlotHub = usePluginSlotHub()
function BasicLayout({project = 'core'}:{project:string}){
const navigator = useNavigate()
const location = useLocation()
const currentUrl = location.pathname
const { state,accessData,checkPermission,accessInit,dispatch,resetAccess,getGlobalAccessData, menuList} = useGlobalContext()
const [pathname, setPathname] = useState(currentUrl);
const mainPage = project === 'core' ?'/service/list':'/serviceHub/list'
const [menuItems, setMenuItems] = useState<MenuProps['items']>();
const pluginSlotHub = usePluginSlotHub()
useEffect(()=>{
useEffect(() => {
const newMenu = transformMenuData(menuList)
setMenuItems(newMenu);
},[menuList, state.language,accessInit])
setMenuItems(newMenu)
}, [menuList, state.language, accessInit])
useEffect(() => {
if (currentUrl === '/') {
navigator(mainPage)
}
}, [currentUrl]);
}, [currentUrl])
const headerMenuData = useMemo(() => {
// 判断权限
const hasAccess = (access: unknown) => checkPermission(access as keyof typeof PERMISSION_DEFINITION[0]);
const hasAccess = (access: unknown) => checkPermission(access as keyof (typeof PERMISSION_DEFINITION)[0])
// 过滤菜单项
const filterMenu = (menu: Array<{ [k: string]: unknown }>) => {
return [...menu]
.filter(x => x) // 过滤掉空数据
.filter((x) => x) // 过滤掉空数据
.map((item: any) => {
if (item.routes && item.routes.length > 0) {
// 递归处理子菜单
const filteredRoutes: Array<{ [k: string]: unknown }> = filterMenu(item.routes);
const filteredRoutes: Array<{ [k: string]: unknown }> = filterMenu(item.routes)
if (filteredRoutes.length === 0) {
return false
}
return { ...item,routes: filteredRoutes,name:$t(item.name) };
return { ...item, routes: filteredRoutes, name: $t(item.name) }
}
// 处理没有 routes 的菜单项
if (item.access) {
return (item.access === 'all' || hasAccess(item.access)) ? {...item,name:$t(item.name)} : null;
return item.access === 'all' || hasAccess(item.access) ? { ...item, name: $t(item.name) } : null
}
// 如果没有 access 和 routes,则保留
return {...item,name:$t(item.name) };
})
.filter(x => x); // 过滤掉处理后为 null 的项
};
// 初始过滤操作
const res = [...(menuItems || [])]!.filter(x => x).map((x: any) => (x.routes ? { ...x,name:$t(x.name), routes: filterMenu(x.routes) } : {...x,name:$t(x.name)}));
// 返回处理后的数据
return { path: '/', routes: res.map(x=> ({...x, routes: x.routes?.filter(x=> (x.access || x.routes?.length > 0))})).filter(x=> (x.access || x.routes?.length > 0)) };
}, [accessData, state.language,menuItems]);
const { message } = App.useApp()
const [userInfo,setUserInfo] = useState<UserInfoType>()
const {fetchData} = useFetch()
const navigate = useNavigate();
// 如果没有 access 和 routes,则保留
return { ...item, name: $t(item.name) }
})
.filter((x) => x) // 过滤掉处理后为 null 的项
}
// 初始过滤操作
const res = [...(menuItems || [])]!
.filter((x) => x)
.map((x: any) =>
x.routes ? { ...x, name: $t(x.name), routes: filterMenu(x.routes) } : { ...x, name: $t(x.name) }
)
// 返回处理后的数据
return {
path: '/',
routes: res
.map((x) => ({ ...x, routes: x.routes?.filter((x) => x.access || x.routes?.length > 0) }))
.filter((x) => x.access || x.routes?.length > 0)
}
}, [accessData, state.language, menuItems])
const { message } = App.useApp()
const [userInfo, setUserInfo] = useState<UserInfoType>()
const { fetchData } = useFetch()
const navigate = useNavigate()
const getUserInfo = () => {
fetchData<BasicResponse<{ profile: UserInfoType }>>('account/profile', { method: 'GET' })
.then(response => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setUserInfo(data.profile)
dispatch({ type: 'UPDATE_USERDATA', userData: data.profile })
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
fetchData<BasicResponse<{ profile: UserInfoType }>>('account/profile', { method: 'GET' }).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setUserInfo(data.profile)
dispatch({ type: 'UPDATE_USERDATA', userData: data.profile })
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
useEffect(() => {
getUserInfo()
getGlobalAccessData()
}, []);
}, [])
const logOut = () => {
fetchData<BasicResponse<null>>('account/logout', { method: 'GET' }).then(response => {
fetchData<BasicResponse<null>>('account/logout', { method: 'GET' }).then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
dispatch({ type: 'LOGOUT' })
@@ -130,133 +130,162 @@ const themeToken = {
})
}
const items: MenuProps['items'] = useMemo(() => [
userInfo?.type !== 'guest' && {
key: '2',
label: (
<Button key="changePsw" type="text" className="flex items-center p-0 bg-transparent border-none " onClick={() => navigator('/userProfile/changepsw')}>
{$t('账号设置')}
</Button>)
},
{
key: '3',
label: (
<Button key="logout" type="text" className="flex items-center p-0 bg-transparent border-none " onClick={logOut}>
{$t('退出登录')}
</Button>)
},
].filter(Boolean), [userInfo]);
const items: MenuProps['items'] = useMemo(
() =>
[
userInfo?.type !== 'guest' && {
key: '2',
label: (
<Button
key="changePsw"
type="text"
className="flex items-center p-0 bg-transparent border-none"
onClick={() => navigator('/userProfile/changepsw')}
>
{$t('账号设置')}
</Button>
)
},
{
key: '3',
label: (
<Button
key="logout"
type="text"
className="flex items-center p-0 bg-transparent border-none"
onClick={logOut}
>
{$t('退出登录')}
</Button>
)
}
].filter(Boolean),
[userInfo]
)
const actionRender = useMemo(() => {
return [
<LanguageSetting />,
<Button
className=" text-[#ffffffb3] hover:text-[#fff] border-none"
type="default"
ghost
onClick={() => {
window.open('https://docs.apipark.com', '_blank')
}}
>
<span className="flex items-center gap-[8px]">
{' '}
<Icon icon="ic:baseline-help" width="14" height="14" />
{$t('文档')}
</span>
</Button>,
...((pluginSlotHub.getSlot('basicLayoutAfterBtns') as unknown[]) || [])
]
}, [pluginSlotHub.getSlot('basicLayoutAfterBtns')])
const actionRender =useMemo( ()=>{
return [
<LanguageSetting />,
<Button className=" text-[#ffffffb3] hover:text-[#fff] border-none" type="default" ghost onClick={()=>{window.open('https://docs.apipark.com','_blank')}}>
<span className='flex items-center gap-[8px]'> <Icon icon="ic:baseline-help" width="14" height="14"/>{$t('文档')}</span>
</Button> ,
...((pluginSlotHub.getSlot('basicLayoutAfterBtns') as unknown[] )||[] )
]
},[pluginSlotHub.getSlot('basicLayoutAfterBtns') ])
return(
<div
id="test-pro-layout"
style={{
height: '100vh',
overflow: 'auto',
}}
return (
<div
id="test-pro-layout"
style={{
height: '100vh',
overflow: 'auto'
}}
>
<ProConfigProvider hashed={false}>
<ConfigProvider
getTargetContainer={() => {
return document.getElementById('test-pro-layout') || document.body
}}
>
<ProConfigProvider hashed={false}>
<ConfigProvider
getTargetContainer={() => {
return document.getElementById('test-pro-layout') || document.body;
<ProLayout
prefixCls="apipark-layout"
location={{
pathname
}}
siderWidth={220}
breakpoint={'lg'}
route={headerMenuData}
token={themeToken}
siderMenuType="group"
menu={{
type: 'group',
collapsedShowGroupTitle: true
}}
disableMobile={true}
avatarProps={{
src: AvatarPic || userInfo?.avatar,
size: 'small',
title: userInfo?.username || 'unknown',
render: (props, dom) => {
return (
<Dropdown
menu={{
items
}}
>
<ProLayout
prefixCls="apipark-layout"
location={{
pathname,
}}
siderWidth={220}
breakpoint={'lg'}
route={headerMenuData}
token={themeToken}
siderMenuType="group"
menu={{
type: 'group',
collapsedShowGroupTitle: true,
}}
disableMobile={true}
avatarProps={{
src: AvatarPic || userInfo?.avatar,
size: 'small',
title: userInfo?.username||'unknown',
render: (props, dom) => {
return (
<Dropdown
menu={{
items
}}
>
<div className='avatar-dom'>{dom}
</div>
</Dropdown>
);
},
}}
actionsRender={(props) => {
if (props.isMobile) return [];
if (typeof window === 'undefined') return [];
return actionRender;
}}
headerTitleRender={() => (
<div className="w-[192px] flex items-center">
<img
className="h-[20px] cursor-pointer "
src={Logo}
onClick={()=> navigator(mainPage)}
/>
</div>
)}
logo={Logo}
pageTitleRender={()=>$t('APIPark')}
menuFooterRender={(props) => {
if (props?.collapsed) return undefined;
}}
menuItemRender={(item, dom) => (
<div
onClick={() => {
// 同级目录点击无效
if(item.key && routerKeyMap.get(item.key) && routerKeyMap.get(item.key).length > 0 && routerKeyMap.get(item.key)?.indexOf(pathname.split('/')[1]) !== -1){
return
}
if(item.key === pathname.split('/')[1]){
return
}
if(item.path){
navigator(item.path)
}
setPathname(item.path || '');
}}
>
{dom}
</div>
)}
fixSiderbar={true}
layout='mix'
splitMenus={true}
collapsed={false}
collapsedButtonRender={false}
>
<div className={`w-full h-calc-100vh-minus-navbar pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T ${currentUrl.startsWith('/role/list') ? 'overflow-auto' : 'overflow-hidden' }`}>
<Outlet />
</div>
</ProLayout>
</ConfigProvider>
</ProConfigProvider>
</div>
)
>
<div className="avatar-dom">{dom}</div>
</Dropdown>
)
}
}}
actionsRender={(props) => {
if (props.isMobile) return []
if (typeof window === 'undefined') return []
return actionRender
}}
headerTitleRender={() => (
<div className="w-[192px] flex items-center">
<img className="h-[20px] cursor-pointer " src={Logo} onClick={() => navigator(mainPage)} />
</div>
)}
logo={Logo}
pageTitleRender={() => $t('APIPark')}
menuFooterRender={(props) => {
if (props?.collapsed) return undefined
}}
menuItemRender={(item, dom) => (
<div
onClick={() => {
// 同级目录点击无效
if (
item.key &&
routerKeyMap.get(item.key) &&
routerKeyMap.get(item.key).length > 0 &&
routerKeyMap.get(item.key)?.indexOf(pathname.split('/')[1]) !== -1
) {
return
}
if (item.key === pathname.split('/')[1]) {
return
}
if (item.path) {
navigator(item.path)
}
setPathname(item.path || '')
}}
>
{dom}
</div>
)}
fixSiderbar={true}
layout="mix"
splitMenus={true}
collapsed={false}
collapsedButtonRender={false}
>
<div
className={`w-full h-calc-100vh-minus-navbar pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T ${
currentUrl.startsWith('/role/list') ? 'overflow-auto' : 'overflow-hidden'
}`}
>
<Outlet />
</div>
</ProLayout>
</ConfigProvider>
</ProConfigProvider>
</div>
)
}
export default BasicLayout
export default BasicLayout
@@ -1,15 +1,11 @@
import { Breadcrumb } from "antd"
import { useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {FC,useEffect} from "react";
import { Breadcrumb } from 'antd'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import { FC, useEffect } from 'react'
const TopBreadcrumb: FC = () => {
const { breadcrumb } = useBreadcrumb()
useEffect(() => {
}, [breadcrumb]);
return (
<Breadcrumb items={breadcrumb} />
)
const { breadcrumb } = useBreadcrumb()
useEffect(() => {}, [breadcrumb])
return <Breadcrumb items={breadcrumb} />
}
export default TopBreadcrumb
export default TopBreadcrumb
@@ -1,34 +1,32 @@
import { FC } from 'react';
import { Table } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { $t } from '@common/locales';
import { FC } from 'react'
import { Table } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { $t } from '@common/locales'
interface DataType {
httpStatusCode: string;
systemStatusCode: string;
description: string;
httpStatusCode: string
systemStatusCode: string
description: string
}
const columns: ColumnsType<DataType> = [
{
title:$t('HTTP 状态码'),
title: $t('HTTP 状态码'),
dataIndex: 'httpStatusCode',
key: 'httpStatusCode',
key: 'httpStatusCode'
},
{
title:$t('系统状态码'),
title: $t('系统状态码'),
dataIndex: 'systemStatusCode',
key: 'systemStatusCode',
key: 'systemStatusCode'
},
{
title: $t('描述'),
dataIndex: 'description',
key: 'description',
ellipsis:true
},
];
ellipsis: true
}
]
const data: DataType[] = [
// {
@@ -44,12 +42,12 @@ const data: DataType[] = [
{
httpStatusCode: '413',
systemStatusCode: '10003',
description: '请求频率过高',
description: '请求频率过高'
},
{
httpStatusCode: '403',
systemStatusCode: '10004',
description: '请求来源非法,不在白名单中',
description: '请求来源非法,不在白名单中'
},
// {
// httpStatusCode: '416',
@@ -59,7 +57,7 @@ const data: DataType[] = [
{
httpStatusCode: '504',
systemStatusCode: '10006',
description: '网关超时',
description: '网关超时'
},
// {
// httpStatusCode: '504',
@@ -69,7 +67,7 @@ const data: DataType[] = [
{
httpStatusCode: '404',
systemStatusCode: '10007',
description: '接口不存在',
description: '接口不存在'
},
// {
// httpStatusCode: '416',
@@ -84,42 +82,43 @@ const data: DataType[] = [
{
httpStatusCode: '400',
systemStatusCode: '10010',
description: '无法识别请求内容,请检查请求体是否正确',
description: '无法识别请求内容,请检查请求体是否正确'
},
{
httpStatusCode: '400',
systemStatusCode: '10011',
description: '请求头部缺少 Content-Type 字段',
description: '请求头部缺少 Content-Type 字段'
},
{
httpStatusCode: '400',
systemStatusCode: '10011',
description: '请求头部 Content-Type 字段错误',
description: '请求头部 Content-Type 字段错误'
},
{
httpStatusCode: '400',
systemStatusCode: '10014',
description: '批量参数超出单次批量数量的最大限制',
description: '批量参数超出单次批量数量的最大限制'
},
{
httpStatusCode: '400',
systemStatusCode: '10016',
description: '参数缺少内容',
description: '参数缺少内容'
},
{
httpStatusCode: '500',
systemStatusCode: '10017',
description: '参数类型错误',
},
];
description: '参数类型错误'
}
]
const CodePage: FC = () =>
<Table
const CodePage: FC = () => (
<Table
size="small"
columns={columns}
className='table-border border-b-0 rounded'
dataSource={data?.map((item, index) => ({...item, key: index})) || []}
columns={columns}
className="table-border border-b-0 rounded"
dataSource={data?.map((item, index) => ({ ...item, key: index })) || []}
pagination={false}
/>;
/>
)
export default CodePage;
export default CodePage
@@ -1,33 +1,32 @@
import { useState,FC } from 'react';
import { Tooltip, Button } from 'antd';
import useCopyToClipboard from '@common/hooks/copy';
import { Icon } from '@iconify/react/dist/iconify.js';
import { useState, FC } from 'react'
import { Tooltip, Button } from 'antd'
import useCopyToClipboard from '@common/hooks/copy'
import { Icon } from '@iconify/react/dist/iconify.js'
type AddressItem = {
expand?: boolean;
[key: string]: unknown;
expand?: boolean
[key: string]: unknown
}
type CopyAddrListProps = {
addrItem: AddressItem;
onAddrItemChange?: (addrItem: AddressItem) => void;
keyName: string;
type CopyAddrListProps = {
addrItem: AddressItem
onAddrItemChange?: (addrItem: AddressItem) => void
keyName: string
}
const CopyAddrList: FC<CopyAddrListProps> = ({ addrItem, onAddrItemChange, keyName }) => {
const [localAddrItem, setLocalAddrItem] = useState<AddressItem>(addrItem);
const { copyToClipboard } = useCopyToClipboard();
const [localAddrItem, setLocalAddrItem] = useState<AddressItem>(addrItem)
const { copyToClipboard } = useCopyToClipboard()
const toggleExpand = () => {
const updatedAddrItem = { ...localAddrItem, expand: !localAddrItem.expand };
setLocalAddrItem(updatedAddrItem);
onAddrItemChange?.(updatedAddrItem);
};
const updatedAddrItem = { ...localAddrItem, expand: !localAddrItem.expand }
setLocalAddrItem(updatedAddrItem)
onAddrItemChange?.(updatedAddrItem)
}
const renderTooltipTitle = () => {
// 假设keyName对应的值是一个字符串数组
const addresses:string[] = localAddrItem[keyName] as string[]
const addresses: string[] = localAddrItem[keyName] as string[]
return (
<div>
{addresses?.map((addr, index) => (
@@ -36,29 +35,41 @@ const CopyAddrList: FC<CopyAddrListProps> = ({ addrItem, onAddrItemChange, keyNa
</div>
))}
</div>
);
};
)
}
const renderAddresses = () => {
if (!localAddrItem.expand) {
return (
<span className="overflow-ellipsis w-full inline-block overflow-hidden align-middle">
<Tooltip title={renderTooltipTitle}>
<span className='flex items-center'>
<span className={`overflow-ellipsis inline-block overflow-hidden align-middle ${((localAddrItem[keyName] as string[]).length > 1) ? 'w-5/6' : 'w-full'}`}>
<span className="flex items-center">
<span
className={`overflow-ellipsis inline-block overflow-hidden align-middle ${(localAddrItem[keyName] as string[]).length > 1 ? 'w-5/6' : 'w-full'}`}
>
{(localAddrItem[keyName] as string[]).join(',')}
</span>
{(localAddrItem[keyName] as string[]).length === 1 && (
<Button type="primary" className="border-none ant-typography-copy text-theme hover:text-A_HOVER " ghost onClick={() => copyToClipboard((localAddrItem[keyName] as string))} icon={<Icon icon="ic:baseline-file-copy" width="14" height="14"/>} size="small" />
<Button
type="primary"
className="border-none ant-typography-copy text-theme hover:text-A_HOVER "
ghost
onClick={() => copyToClipboard(localAddrItem[keyName] as string)}
icon={<Icon icon="ic:baseline-file-copy" width="14" height="14" />}
size="small"
/>
)}
{(localAddrItem[keyName] as string[]).length !== 1 && (
<Button className="border-none bg-transparent w-[16px] h-[22px] text-table_text p-[0px]" icon={<iconpark-icon name="zhankai" style={{marginTop:'4px'}}></iconpark-icon>} onClick={toggleExpand} />
<Button
className="border-none bg-transparent w-[16px] h-[22px] text-table_text p-[0px]"
icon={<iconpark-icon name="zhankai" style={{ marginTop: '4px' }}></iconpark-icon>}
onClick={toggleExpand}
/>
)}
</span>
</Tooltip>
</span>
);
)
} else {
return (
<div className="flex flex-nowrap items-center justify-between">
@@ -66,21 +77,28 @@ const CopyAddrList: FC<CopyAddrListProps> = ({ addrItem, onAddrItemChange, keyNa
{(localAddrItem[keyName] as string[])?.map((addr: string, index: number) => (
<div key={index} className="block w-full">
<span className="leading-6">{addr}</span>
<Button type="primary" className="border-none bg-transparent w-[16px] h-[22px] p-[0px] ml-2 text-theme hover:text-A_HOVER" ghost onClick={() => copyToClipboard(addr)} icon={<Icon icon="ic:baseline-file-copy" width="14" height="14"/>} size="small" />
<Button
type="primary"
className="border-none bg-transparent w-[16px] h-[22px] p-[0px] ml-2 text-theme hover:text-A_HOVER"
ghost
onClick={() => copyToClipboard(addr)}
icon={<Icon icon="ic:baseline-file-copy" width="14" height="14" />}
size="small"
/>
</div>
))}
</div>
<Button className="border-none bg-transparent w-[16px] h-[22px] text-table_text p-[0px]" icon={<iconpark-icon name="shouqi-2"></iconpark-icon>} onClick={toggleExpand} />
<Button
className="border-none bg-transparent w-[16px] h-[22px] text-table_text p-[0px]"
icon={<iconpark-icon name="shouqi-2"></iconpark-icon>}
onClick={toggleExpand}
/>
</div>
);
)
}
};
}
return (
<div>
{renderAddresses()}
</div>
);
};
return <div>{renderAddresses()}</div>
}
export default CopyAddrList;
export default CopyAddrList
@@ -1,59 +1,84 @@
import { Button, Drawer, DrawerProps, Space } from "antd";
import WithPermission from "./WithPermission";
import { useEffect, useState } from "react";
import { $t } from '@common/locales';
import { Button, Drawer, DrawerProps, Space } from 'antd'
import WithPermission from './WithPermission'
import { useEffect, useState } from 'react'
import { $t } from '@common/locales'
export type DrawerWithFooterProps = DrawerProps & {
onSubmit?: () => Promise<boolean|string>|undefined
submitAccess?: string
submitDisabled?:boolean
onClose?:()=>void
showLastStep?:boolean
onLastStep?:()=>void
notAutoClose?:boolean
showOkBtn?:boolean
extraBtn?:React.ReactNode
okBtnTitle?:string
cancelBtnTitle?:string
onSubmit?: () => Promise<boolean | string> | undefined
submitAccess?: string
submitDisabled?: boolean
onClose?: () => void
showLastStep?: boolean
onLastStep?: () => void
notAutoClose?: boolean
showOkBtn?: boolean
extraBtn?: React.ReactNode
okBtnTitle?: string
cancelBtnTitle?: string
}
export function DrawerWithFooter(props:DrawerWithFooterProps){
const {children,title,placement='right',onClose,onSubmit,submitDisabled = false,okBtnTitle= $t('提交'),cancelBtnTitle,open,submitAccess,showLastStep,onLastStep,notAutoClose,showOkBtn=true,extraBtn} = props
const [submitLoading, setSubmitLoading] = useState<boolean>(false)
const handlerSubmit = ()=>{
setSubmitLoading(true)
onSubmit?.()?.then(()=>{!notAutoClose && onClose?.()}).finally(()=>{setSubmitLoading(false)})
}
export function DrawerWithFooter(props: DrawerWithFooterProps) {
const {
children,
title,
placement = 'right',
onClose,
onSubmit,
submitDisabled = false,
okBtnTitle = $t('提交'),
cancelBtnTitle,
open,
submitAccess,
showLastStep,
onLastStep,
notAutoClose,
showOkBtn = true,
extraBtn
} = props
const [submitLoading, setSubmitLoading] = useState<boolean>(false)
const handlerSubmit = () => {
setSubmitLoading(true)
onSubmit?.()
?.then(() => {
!notAutoClose && onClose?.()
})
.finally(() => {
setSubmitLoading(false)
})
}
useEffect(()=>{!open && setSubmitLoading(false)},[open])
return (<>
<Drawer
{...props}
push={false}
title={title}
placement={placement}
width="60%"
destroyOnClose={true}
maskClosable={false}
classNames={
{footer:'text-right'}
}
footer={
<Space className="flex flex-row-reverse" style={{}}>
{showOkBtn && <WithPermission access={submitAccess}>
<Button onClick={handlerSubmit} type="primary" loading={submitLoading} disabled={submitDisabled}>
{ okBtnTitle}
</Button>
</WithPermission>}
{ showLastStep && <Button onClick={onLastStep ?? onClose}> { $t('上一步')}</Button>}
{ extraBtn }
<Button onClick={onClose}>{cancelBtnTitle ?? (showOkBtn ? $t('取消'):$t('关闭'))}</Button>
</Space>
}
onClose={onClose}
open={open}
>
{children}
</Drawer>
</>)
}
useEffect(() => {
!open && setSubmitLoading(false)
}, [open])
return (
<>
<Drawer
{...props}
push={false}
title={title}
placement={placement}
width="60%"
destroyOnClose={true}
maskClosable={false}
classNames={{ footer: 'text-right' }}
footer={
<Space className="flex flex-row-reverse" style={{}}>
{showOkBtn && (
<WithPermission access={submitAccess}>
<Button onClick={handlerSubmit} type="primary" loading={submitLoading} disabled={submitDisabled}>
{okBtnTitle}
</Button>
</WithPermission>
)}
{showLastStep && <Button onClick={onLastStep ?? onClose}> {$t('上一步')}</Button>}
{extraBtn}
<Button onClick={onClose}>{cancelBtnTitle ?? (showOkBtn ? $t('取消') : $t('关闭'))}</Button>
</Space>
}
onClose={onClose}
open={open}
>
{children}
</Drawer>
</>
)
}
@@ -1,91 +1,93 @@
import {FC } from 'react';
import { Input, Space } from 'antd';
import { Icon } from '@iconify/react/dist/iconify.js';
import { FC } from 'react'
import { Input, Space } from 'antd'
import { Icon } from '@iconify/react/dist/iconify.js'
type KeyValueInput = {
key: string;
value: string;
};
key: string
value: string
}
type DynamicKeyValueInputProps = {
value?: KeyValueInput[];
onChange?: (newValue: KeyValueInput[]) => void;
};
value?: KeyValueInput[]
onChange?: (newValue: KeyValueInput[]) => void
}
export function transferToList (rawData:unknown):Array<{key:string, value:string}> {
const res:Array<{key:string, value:string}> = []
if(!rawData)
return res
const keys:Array<string> = Object.keys(rawData)
export function transferToList(rawData: unknown): Array<{ key: string; value: string }> {
const res: Array<{ key: string; value: string }> = []
if (!rawData) return res
const keys: Array<string> = Object.keys(rawData)
if (keys?.length > 0) {
for (const key of keys) {
res.push({ key: key, value: rawData[key] })
for (const key of keys) {
res.push({ key: key, value: rawData[key] })
}
return [...res, { key: '', value: '' }]
}
return [...res, { key: '', value: '' }]
}
return [{ key: '', value: '' }]
return [{ key: '', value: '' }]
}
export function transferToMap (rawData:Array<{key:string, value:string}>):{[key:string]:string} {
const res:{[key:string]:string} = {}
export function transferToMap(rawData: Array<{ key: string; value: string }>): { [key: string]: string } {
const res: { [key: string]: string } = {}
for (const kv of rawData) {
if (kv.key && kv.value) { res[kv.key] = kv.value }
if (kv.key && kv.value) {
res[kv.key] = kv.value
}
}
return res
}
export const DynamicKeyValueInput: FC<DynamicKeyValueInputProps> = ({value = [{key:'',value:''}],onChange}) => {
export const DynamicKeyValueInput: FC<DynamicKeyValueInputProps> = ({ value = [{ key: '', value: '' }], onChange }) => {
// const [keyValuePairs, setKeyValuePairs] = useState<KeyValueInput[]>([{ key: '', value: '' }]);
// Define a handler for when the inputs change
// Define a handler for when the inputs change
const handleInputChange = (index: number, type: 'key' | 'value', newValue: string) => {
// Create a new array with the updated value
const newKeyValuePairs = value ? [...value] : [];
const newKeyValuePairs = value ? [...value] : []
if (newKeyValuePairs[index]) {
newKeyValuePairs[index][type] = newValue;
newKeyValuePairs[index][type] = newValue
// If we're changing the last input and it's not empty, add a new pair
if (index === newKeyValuePairs.length - 1 && (newKeyValuePairs[index].key || newKeyValuePairs[index].value)) {
newKeyValuePairs.push({ key: '', value: '' });
newKeyValuePairs.push({ key: '', value: '' })
}
// Call the onChange handler if it exists
onChange?.(newKeyValuePairs);
onChange?.(newKeyValuePairs)
}
};
}
const addNewPair = () => {
const newKeyValuePairs = value ? [...value, { key: '', value: '' }] : [{ key: '', value: '' }];
onChange?.(newKeyValuePairs);
};
const newKeyValuePairs = value ? [...value, { key: '', value: '' }] : [{ key: '', value: '' }]
onChange?.(newKeyValuePairs)
}
const removePair = (index: number) => {
const newKeyValuePairs = value?.filter((_, idx) => idx !== index) || [];
onChange?.(newKeyValuePairs);
};
const newKeyValuePairs = value?.filter((_, idx) => idx !== index) || []
onChange?.(newKeyValuePairs)
}
return (
<>
{value && value?.map((pair, index) => (
<Space key={index} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
<Input
placeholder="Key"
value={pair.key}
onChange={(e) => handleInputChange(index, 'key', e.target.value)}
style={{ width: 162 }} />
<Input
placeholder="Value"
value={pair.value}
onChange={(e) => handleInputChange(index, 'value', e.target.value)}
style={{ width: 162 }} />
{index !== value.length - 1 && (
<>
<Icon icon="ic:baseline-delete" onClick={() => removePair(index)} width="14" height="14"/>
<Icon icon="ic:baseline-add" onClick={addNewPair} width="14" height="14"/>
</>
)}
</Space>
<>
{value &&
value?.map((pair, index) => (
<Space key={index} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
<Input
placeholder="Key"
value={pair.key}
onChange={(e) => handleInputChange(index, 'key', e.target.value)}
style={{ width: 162 }}
/>
<Input
placeholder="Value"
value={pair.value}
onChange={(e) => handleInputChange(index, 'value', e.target.value)}
style={{ width: 162 }}
/>
{index !== value.length - 1 && (
<>
<Icon icon="ic:baseline-delete" onClick={() => removePair(index)} width="14" height="14" />
<Icon icon="ic:baseline-add" onClick={addNewPair} width="14" height="14" />
</>
)}
</Space>
))}
</>
);
};
</>
)
}
@@ -1,116 +1,130 @@
import { EditableProTable } from "@ant-design/pro-components";
import { useState, useEffect, useMemo } from "react";
import { v4 as uuidv4} from 'uuid';
import { PageProColumns } from "./PageList";
import TableBtnWithPermission from "./TableBtnWithPermission";
import { $t } from "@common/locales";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { EditableProTable } from '@ant-design/pro-components'
import { useState, useEffect, useMemo } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { PageProColumns } from './PageList'
import TableBtnWithPermission from './TableBtnWithPermission'
import { $t } from '@common/locales'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
interface EditableTableProps<T> {
configFields: PageProColumns<T>[];
value?: T[]; // 外部传入的值
className?: string;
onChange?: (newConfigItems: T[]) => void; // 当配置项变化时,外部传入的回调函数
// tableProps?: TableProps<T>;
disabled?:boolean
extendsId?:string[] // 自增一行时,需要和上一行数据一致的字段,比如集群id
configFields: PageProColumns<T>[]
value?: T[] // 外部传入的值
className?: string
onChange?: (newConfigItems: T[]) => void // 当配置项变化时,外部传入的回调函数
// tableProps?: TableProps<T>;
disabled?: boolean
extendsId?: string[] // 自增一行时,需要和上一行数据一致的字段,比如集群id
}
const EditableTable = <T extends { _id: string }>({
configFields,
value, // value 现在是外部传入的配置项数组
onChange, // onChange 现在是当配置项数组变化时的回调函数
// tableProps,
disabled,
className,
extendsId,
}: EditableTableProps<T>) => {
const [configurations, setConfigurations] = useState<(T | {_id:string})[]>(value ||[{_id:'1234'}]);
const {state} = useGlobalContext()
configFields,
value, // value 现在是外部传入的配置项数组
onChange, // onChange 现在是当配置项数组变化时的回调函数
// tableProps,
disabled,
className,
extendsId
}: EditableTableProps<T>) => {
const [configurations, setConfigurations] = useState<(T | { _id: string })[]>(value || [{ _id: '1234' }])
const { state } = useGlobalContext()
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() =>
value?.map((item) => item._id) || ['1234']
);
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() => value?.map((item) => item._id) || ['1234'])
useEffect(() => {
setConfigurations(value?.map((x)=>x._id ? x : {...x,_id:uuidv4()}) || [{_id:uuidv4()}]);
}, [value]);
useEffect(() => {
setConfigurations(value?.map((x) => (x._id ? x : { ...x, _id: uuidv4() })) || [{ _id: uuidv4() }])
}, [value])
const getNotEmptyValue = (value:unknown)=>{
return value
}
const getNotEmptyValue = (value: unknown) => {
return value
}
const translatedColumns = useMemo(()=>configFields.map((x)=>({...x, title:$t(x.title as string)})),[state.language,configFields])
const translatedColumns = useMemo(
() => configFields.map((x) => ({ ...x, title: $t(x.title as string) })),
[state.language, configFields]
)
return (
<EditableProTable<T>
className={className}
columns={translatedColumns}
rowKey="_id"
value={configurations as T[]}
size="small"
bordered={true}
recordCreatorProps={false}
editable={ {
type: 'multiple',
editableKeys:disabled ? [] : configurations?.map(x=>x._id),
actionRender: (row, config) => {
return [
<TableBtnWithPermission key="add" btnType="add" onClick={() => {
const newId = uuidv4();
setConfigurations((prev)=>{
const tmpPreData = [...prev];
const newId = uuidv4()
const lastRecord:{[k:string]:unknown} = tmpPreData[tmpPreData.length - 1];
const newRecord :{[k:string]:unknown, _id:string}= { _id: newId };
// 当extendsId的长度大于0时,根据extendsId指定的字段从最后一个record中复制值
if(extendsId && extendsId.length > 0) {
extendsId.forEach(field => {
newRecord[field] = lastRecord[field];
});
}
tmpPreData.splice(Number(config.index) + 1, 0,newRecord);
onChange?.(getNotEmptyValue(tmpPreData));
return tmpPreData});
setEditableRowKeys((prev)=>([...prev,newId]))
}}
btnTitle="增加"/>,
return (
<EditableProTable<T>
className={className}
columns={translatedColumns}
rowKey="_id"
value={configurations as T[]}
size="small"
bordered={true}
recordCreatorProps={false}
editable={{
type: 'multiple',
editableKeys: disabled ? [] : configurations?.map((x) => x._id),
actionRender: (row, config) => {
return [
<TableBtnWithPermission
key="add"
btnType="add"
onClick={() => {
const newId = uuidv4()
setConfigurations((prev) => {
const tmpPreData = [...prev]
const newId = uuidv4()
const lastRecord: { [k: string]: unknown } = tmpPreData[tmpPreData.length - 1]
const newRecord: { [k: string]: unknown; _id: string } = { _id: newId }
(config.index !== configurations.length - 1 )&& <TableBtnWithPermission key="remove" btnType="remove" btnTitle="删除"
onClick={() => {
setConfigurations((prev)=>{
const tmpPreData = [...prev];
tmpPreData.splice(Number(config.index), 1);
onChange?.(tmpPreData);
return tmpPreData});
setEditableRowKeys((prev)=>(prev.filter(x=>x !== config._id)))
}}/>,,
];
},
onValuesChange: (record, recordList) => {
if(record._id === recordList[recordList.length - 1]._id){
const newId = uuidv4()
const lastRecord:{[k:string]:unknown} = recordList[recordList.length - 1];
const newRecord :{[k:string]:unknown, _id:string}= { _id: newId };
// 当extendsId的长度大于0时,根据extendsId指定的字段从最后一个record中复制值
if(extendsId && extendsId.length > 0) {
extendsId.forEach(field => {
newRecord[field] = lastRecord[field];
});
}
recordList = ([...recordList, newRecord as T]);
setEditableRowKeys((prev)=>[...prev, newId])
}
setConfigurations(recordList);
onChange?.(recordList);
},
onChange: setEditableRowKeys,
}}
/>
)
}
// 当extendsId的长度大于0时,根据extendsId指定的字段从最后一个record中复制值
if (extendsId && extendsId.length > 0) {
extendsId.forEach((field) => {
newRecord[field] = lastRecord[field]
})
}
tmpPreData.splice(Number(config.index) + 1, 0, newRecord)
onChange?.(getNotEmptyValue(tmpPreData))
return tmpPreData
})
setEditableRowKeys((prev) => [...prev, newId])
}}
btnTitle="增加"
/>,
export default EditableTable;
config.index !== configurations.length - 1 && (
<TableBtnWithPermission
key="remove"
btnType="remove"
btnTitle="删除"
onClick={() => {
setConfigurations((prev) => {
const tmpPreData = [...prev]
tmpPreData.splice(Number(config.index), 1)
onChange?.(tmpPreData)
return tmpPreData
})
setEditableRowKeys((prev) => prev.filter((x) => x !== config._id))
}}
/>
),
,
]
},
onValuesChange: (record, recordList) => {
if (record._id === recordList[recordList.length - 1]._id) {
const newId = uuidv4()
const lastRecord: { [k: string]: unknown } = recordList[recordList.length - 1]
const newRecord: { [k: string]: unknown; _id: string } = { _id: newId }
// 当extendsId的长度大于0时,根据extendsId指定的字段从最后一个record中复制值
if (extendsId && extendsId.length > 0) {
extendsId.forEach((field) => {
newRecord[field] = lastRecord[field]
})
}
recordList = [...recordList, newRecord as T]
setEditableRowKeys((prev) => [...prev, newId])
}
setConfigurations(recordList)
onChange?.(recordList)
},
onChange: setEditableRowKeys
}}
/>
)
}
export default EditableTable
@@ -1,105 +1,119 @@
import { EditableFormInstance, EditableProTable } from "@ant-design/pro-components";
import { useState, useEffect, useMemo, useRef, MutableRefObject } from "react";
import { v4 as uuidv4} from 'uuid';
import { PageProColumns } from "./PageList";
import TableBtnWithPermission from "./TableBtnWithPermission";
import { $t } from "@common/locales";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { Form } from "antd";
import { debounce } from "lodash-es";
import { EditableFormInstance, EditableProTable } from '@ant-design/pro-components'
import { useState, useEffect, useMemo, useRef, MutableRefObject } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { PageProColumns } from './PageList'
import TableBtnWithPermission from './TableBtnWithPermission'
import { $t } from '@common/locales'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { Form } from 'antd'
import { debounce } from 'lodash-es'
interface EditableTableProps<T> {
configFields: PageProColumns<T>[];
value?: T[]; // 外部传入的值
className?: string;
onChange?: (newConfigItems: T[]) => void; // 当配置项变化时,外部传入的回调函数
// tableProps?: TableProps<T>;
disabled?:boolean
getFromRef?:(form:MutableRefObject<EditableFormInstance<T> | undefined>)=>void
configFields: PageProColumns<T>[]
value?: T[] // 外部传入的值
className?: string
onChange?: (newConfigItems: T[]) => void // 当配置项变化时,外部传入的回调函数
// tableProps?: TableProps<T>;
disabled?: boolean
getFromRef?: (form: MutableRefObject<EditableFormInstance<T> | undefined>) => void
}
const EditableTableNotAutoGen = <T extends { _id: string }>({
configFields,
value, // value 现在是外部传入的配置项数组
onChange, // onChange 现在是当配置项数组变化时的回调函数
// tableProps,
disabled,
className,
getFromRef
}: EditableTableProps<T>) => {
const [configurations, setConfigurations] = useState<(T | {_id:string})[]>(value ||[{_id:'1234'}]);
const {state} = useGlobalContext()
const form =useRef<EditableFormInstance<T>>();
const [tableForm] = Form.useForm();
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() =>
value?.map((item) => item._id) || ['1234']
);
configFields,
value, // value 现在是外部传入的配置项数组
onChange, // onChange 现在是当配置项数组变化时的回调函数
// tableProps,
disabled,
className,
getFromRef
}: EditableTableProps<T>) => {
const [configurations, setConfigurations] = useState<(T | { _id: string })[]>(value || [{ _id: '1234' }])
const { state } = useGlobalContext()
const form = useRef<EditableFormInstance<T>>()
const [tableForm] = Form.useForm()
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() => value?.map((item) => item._id) || ['1234'])
useEffect(()=>{
getFromRef?.(form)
},[form])
useEffect(() => {
getFromRef?.(form)
}, [form])
useEffect(() => {
const newValue = value?.map((x)=>x._id ? x : {...x,_id:uuidv4()}) || [{_id:uuidv4()}]
setConfigurations(newValue);
setTimeout(()=>validateForm(),1000)
}, [value]);
useEffect(() => {
const newValue = value?.map((x) => (x._id ? x : { ...x, _id: uuidv4() })) || [{ _id: uuidv4() }]
setConfigurations(newValue)
setTimeout(() => validateForm(), 1000)
}, [value])
const validateForm = async ()=>{
await tableForm.validateFields();
}
const validateForm = async () => {
await tableForm.validateFields()
}
const translatedColumns = useMemo(()=>configFields.map((x)=>(
{...x,
title:$t(x.title as string),
formItemProps:{
...(x. formItemProps || {}),
rules:[...(x.formItemProps?.rules || []).map((r:Record<string, string>)=>{
if(r.message){
r.message = $t(r.message)
}
return r
})],
}})),[state.language,configFields])
const debouncedOnChange = useMemo(() => debounce((value) => {
onChange?.(value);
}, 500), [onChange]);
const translatedColumns = useMemo(
() =>
configFields.map((x) => ({
...x,
title: $t(x.title as string),
formItemProps: {
...(x.formItemProps || {}),
rules: [
...(x.formItemProps?.rules || []).map((r: Record<string, string>) => {
if (r.message) {
r.message = $t(r.message)
}
return r
})
]
}
})),
[state.language, configFields]
)
return (
<EditableProTable<T>
className={className}
columns={translatedColumns}
onChange={debouncedOnChange}
controlled={true}
rowKey="_id"
value={configurations as T[]}
size="small"
editableFormRef={form}
bordered={true}
recordCreatorProps={false}
editable={ {
type: 'multiple',
form: tableForm,
// errorType:'default',
editableKeys:disabled ? [] : configurations?.map(x=>x._id),
actionRender: (row, config) => {
return [
<TableBtnWithPermission key="delete" btnType="delete" btnTitle="删除"
onClick={() => {
setConfigurations((prev)=>{
const tmpPreData = [...prev];
tmpPreData.splice(Number(config.index), 1);
onChange?.(tmpPreData);
return tmpPreData});
setEditableRowKeys((prev)=>(prev.filter(x=>x !== config._id)))
}}/>,
];
},
onChange: setEditableRowKeys
}}
/>
)
}
const debouncedOnChange = useMemo(
() =>
debounce((value) => {
onChange?.(value)
}, 500),
[onChange]
)
export default EditableTableNotAutoGen;
return (
<EditableProTable<T>
className={className}
columns={translatedColumns}
onChange={debouncedOnChange}
controlled={true}
rowKey="_id"
value={configurations as T[]}
size="small"
editableFormRef={form}
bordered={true}
recordCreatorProps={false}
editable={{
type: 'multiple',
form: tableForm,
// errorType:'default',
editableKeys: disabled ? [] : configurations?.map((x) => x._id),
actionRender: (row, config) => {
return [
<TableBtnWithPermission
key="delete"
btnType="delete"
btnTitle="删除"
onClick={() => {
setConfigurations((prev) => {
const tmpPreData = [...prev]
tmpPreData.splice(Number(config.index), 1)
onChange?.(tmpPreData)
return tmpPreData
})
setEditableRowKeys((prev) => prev.filter((x) => x !== config._id))
}}
/>
]
},
onChange: setEditableRowKeys
}}
/>
)
}
export default EditableTableNotAutoGen
@@ -1,165 +1,195 @@
import {useEffect, useMemo, useState} from 'react';
import { Button, Modal, Form, Table, FormInstance, TableProps, Divider } from 'antd';
import { v4 as uuidv4 } from 'uuid';
import { ColumnsType } from 'antd/es/table';
import WithPermission from './WithPermission';
import { $t } from '@common/locales';
import { COLUMNS_TITLE, VALIDATE_MESSAGE } from '@common/const/const';
import { useGlobalContext } from '@common/contexts/GlobalStateContext';
import TableBtnWithPermission from './TableBtnWithPermission';
import { useEffect, useMemo, useState } from 'react'
import { Button, Modal, Form, Table, FormInstance, TableProps, Divider } from 'antd'
import { v4 as uuidv4 } from 'uuid'
import WithPermission from './WithPermission'
import { $t } from '@common/locales'
import { COLUMNS_TITLE } from '@common/const/const'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import TableBtnWithPermission from './TableBtnWithPermission'
export interface ConfigField<T> {
title: string;
key: keyof T;
component: React.ReactNode;
renderText?: (value: unknown, record: T) => string;
required?: boolean;
ellipsis?:boolean
unRender?:(form:FormInstance)=>boolean
title: string
key: keyof T
component: React.ReactNode
renderText?: (value: unknown, record: T) => string
required?: boolean
ellipsis?: boolean
unRender?: (form: FormInstance) => boolean
}
interface EditableTableWithModalProps<T> {
configFields: ConfigField<T>[];
value?: T[]; // 外部传入的值
className?: string;
onChange?: (newConfigItems: T[]) => void; // 当配置项变化时,外部传入的回调函数
tableProps?: TableProps<T>;
disabled?:boolean
configFields: ConfigField<T>[]
value?: T[] // 外部传入的值
className?: string
onChange?: (newConfigItems: T[]) => void // 当配置项变化时,外部传入的回调函数
tableProps?: TableProps<T>
disabled?: boolean
}
const EditableTableWithModal = <T extends { _id?: string }>({
configFields,
value, // value 现在是外部传入的配置项数组
onChange, // onChange 现在是当配置项数组变化时的回调函数
tableProps,
disabled,
className
}: EditableTableWithModalProps<T>) => {
const [form] = Form.useForm<FormInstance>();
const [isModalVisible, setIsModalVisible] = useState(false);
const [configurations, setConfigurations] = useState<T[]>(value ||[]);
const [editingConfig, setEditingConfig] = useState<T | null>(null);
const {state} = useGlobalContext()
const [formsValue, setFormsValue] = useState<FormInstance<unknown>>()
configFields,
value, // value 现在是外部传入的配置项数组
onChange, // onChange 现在是当配置项数组变化时的回调函数
tableProps,
disabled,
className
}: EditableTableWithModalProps<T>) => {
const [form] = Form.useForm<FormInstance>()
const [isModalVisible, setIsModalVisible] = useState(false)
const [configurations, setConfigurations] = useState<T[]>(value || [])
const [editingConfig, setEditingConfig] = useState<T | null>(null)
const { state } = useGlobalContext()
const [formsValue, setFormsValue] = useState<FormInstance<unknown>>()
const showModal = (config?: T) => {
if (config) {
form.setFieldsValue(config as Record<string, unknown>);
setEditingConfig(config);
const showModal = (config?: T) => {
if (config) {
form.setFieldsValue(config as Record<string, unknown>)
setEditingConfig(config)
} else {
form.resetFields()
setEditingConfig(null)
}
setIsModalVisible(true)
}
const handleCancel = () => {
setIsModalVisible(false)
}
const handleDelete = (_id: string) => {
const newConfigurations = configurations.filter((config) => config._id !== _id)
setConfigurations(newConfigurations)
onChange?.(newConfigurations)
}
const handleOk = () => {
form
.validateFields()
.then((values) => {
let newConfigurations = [...configurations]
if (editingConfig && editingConfig._id) {
newConfigurations = newConfigurations?.map((config) =>
config._id === editingConfig._id ? { ...config, ...values } : config
)
} else {
form.resetFields();
setEditingConfig(null);
const newConfig = { _id: uuidv4(), ...values } as Record<string, unknown>
newConfigurations.push(newConfig as T)
}
setIsModalVisible(true);
};
setConfigurations(newConfigurations)
onChange?.(newConfigurations)
setIsModalVisible(false)
})
.catch((info) => {
console.log('Validate Failed:', info)
})
}
const handleCancel = () => {
setIsModalVisible(false);
};
useEffect(() => {
setConfigurations(value?.map((x) => (x._id ? x : { ...x, _id: uuidv4() })) || [])
}, [value])
const handleDelete = (_id: string) => {
const newConfigurations = configurations.filter(config => config._id !== _id);
setConfigurations(newConfigurations);
onChange?.(newConfigurations);
};
const handleOk = () => {
form.validateFields()
.then(values => {
let newConfigurations = [...configurations];
if (editingConfig && editingConfig._id) {
newConfigurations = newConfigurations?.map(config =>
config._id === editingConfig._id ? { ...config, ...values } : config
);
} else {
const newConfig = { _id: uuidv4(), ...values } as Record<string, unknown>;
newConfigurations.push(newConfig as T);
}
setConfigurations(newConfigurations);
onChange?.(newConfigurations);
setIsModalVisible(false);
})
.catch(info => {
console.log('Validate Failed:', info);
});
};
useEffect(() => {
setConfigurations(value?.map((x)=>x._id ? x : {...x,_id:uuidv4()}) || []);
}, [value]);
const columns = useMemo(()=>[
...configFields.map(({ title, key, renderText }) => ({
title:$t(title),
dataIndex: key as string,
key: key as string,
render: renderText ? (value, record) => $t(renderText(value, record) || '') : undefined,
ellipsis:true
})),
...(disabled ? []:[{
title: COLUMNS_TITLE.operate,
key: 'action',
btnNums:2,
render: (_: unknown, record: T) => (
const columns = useMemo(
() => [
...configFields.map(({ title, key, renderText }) => ({
title: $t(title),
dataIndex: key as string,
key: key as string,
render: renderText ? (value, record) => $t(renderText(value, record) || '') : undefined,
ellipsis: true
})),
...(disabled
? []
: [
{
title: COLUMNS_TITLE.operate,
key: 'action',
btnNums: 2,
render: (_: unknown, record: T) => (
<>
<div className="flex items-center">
<TableBtnWithPermission key="add" disabled={disabled} btnType="edit" onClick={()=>{showModal(record)}} btnTitle='编辑'/>
<div className="flex items-center">
<TableBtnWithPermission
key="add"
disabled={disabled}
btnType="edit"
onClick={() => {
showModal(record)
}}
btnTitle="编辑"
/>
<Divider key="div1" type="vertical" />
<TableBtnWithPermission key="delete" disabled={disabled} btnType="delete" onClick={()=>{handleDelete(record._id || '')}} btnTitle='删除'/>
</div>
<TableBtnWithPermission
key="delete"
disabled={disabled}
btnType="delete"
onClick={() => {
handleDelete(record._id || '')
}}
btnTitle="删除"
/>
</div>
</>
),
}] )
],[state.language, disabled, configFields])
)
}
])
],
[state.language, disabled, configFields]
)
const formItems = useMemo(()=>{
return configFields.map(({ title,key, component, required,unRender }) => {
return (
unRender && unRender(formsValue) ? null :
<Form.Item
label={$t(title as string)}
name={key as string}
rules={[{ required}]}
>
{component}
</Form.Item>
)
})
}
,[formsValue])
const formItems = useMemo(() => {
return configFields.map(({ title, key, component, required, unRender }) => {
return unRender && unRender(formsValue) ? null : (
<Form.Item label={$t(title as string)} name={key as string} rules={[{ required }]}>
{component}
</Form.Item>
)
})
}, [formsValue])
return (
<>
{!disabled && (
<Button className="" disabled={disabled} onClick={() => showModal()}>
{$t('添加配置')}
</Button>
)}
{configurations.length > 0 && (
<Table
className={`mt-btnybase border-solid border-[1px] border-BORDER border-b-0 rounded ${className}`}
{...tableProps}
dataSource={configurations}
size="small"
columns={columns}
rowKey="_id"
pagination={false}
/>
)}
<Modal
title={editingConfig ? $t('编辑配置') : $t('添加配置')}
open={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
width={600}
maskClosable={false}
>
<WithPermission access="">
<Form
form={form}
name="editableTableWithModal"
layout="vertical"
scrollToFirstError
onFieldsChange={() => {
setFormsValue(form.getFieldsValue())
}}
// labelCol={{ span: 7 }}
// wrapperCol={{ span: 17}}
autoComplete="off"
>
{formItems}
</Form>
</WithPermission>
</Modal>
</>
)
}
return (
<>
{!disabled && <Button className="" disabled={disabled} onClick={() => showModal()}>{$t('添加配置')}</Button>}
{configurations.length > 0 &&
<Table
className={`mt-btnybase border-solid border-[1px] border-BORDER border-b-0 rounded ${className}`} {...tableProps} dataSource={configurations} size="small" columns={columns} rowKey="_id" pagination={false}/>}
<Modal
title={editingConfig ? $t('编辑配置') : $t('添加配置')}
open={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
width={600}
maskClosable={false}
>
<WithPermission access=""><Form form={form} name="editableTableWithModal"
layout="vertical"
scrollToFirstError
onFieldsChange={(()=>{
setFormsValue(form.getFieldsValue())
})}
// labelCol={{ span: 7 }}
// wrapperCol={{ span: 17}}
autoComplete="off">
{formItems}
</Form></WithPermission>
</Modal>
</>
);
};
export default EditableTableWithModal;
export default EditableTableWithModal
@@ -1,23 +1,23 @@
import { useState, useEffect } from "react";
import { useState, useEffect } from 'react'
function ErrorBoundary({ children }) {
const [error, setError] = useState(null);
useEffect(() => {
window.addEventListener("error", (event) => {
setError(event.error);
});
}, []);
if (error) {
return (
<div>
<h1>An error occurred</h1>
<pre>{error.message}</pre>
</div>
);
}
return children;
const [error, setError] = useState(null)
useEffect(() => {
window.addEventListener('error', (event) => {
setError(event.error)
})
}, [])
if (error) {
return (
<div>
<h1>An error occurred</h1>
<pre>{error.message}</pre>
</div>
)
}
export default ErrorBoundary
return children
}
export default ErrorBoundary
@@ -1,66 +1,108 @@
import { Button, Tag } from "antd"
import {useNavigate} from "react-router-dom";
import WithPermission from "@common/components/aoplatform/WithPermission";
import { FC, ReactNode } from "react";
import { ArrowLeftOutlined, LeftOutlined } from "@ant-design/icons";
import { $t } from "@common/locales";
import { ArrowLeftOutlined } from '@ant-design/icons'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { $t } from '@common/locales'
import { Button, Tag } from 'antd'
import { FC, ReactNode } from 'react'
import { useNavigate } from 'react-router-dom'
class InsidePageProps {
showBanner?:boolean = true
pageTitle:string| React.ReactNode = ''
tagList?:Array<{label:string|ReactNode}> = []
children:React.ReactNode
showBtn?:boolean = false
btnTitle?:string = ''
description?:string | React.ReactNode= ''
onBtnClick?:()=>void
backUrl?:string = '/'
btnAccess?:string
showBorder?:boolean = true
className?:string = ''
contentClassName?:string=''
headerClassName?:string=''
/** 整个页面滚动 */
scrollPage?:boolean = true
customBtn?:ReactNode
showBanner?: boolean = true
pageTitle: string | React.ReactNode = ''
tagList?: Array<{ label: string | ReactNode }> = []
children: React.ReactNode
showBtn?: boolean = false
btnTitle?: string = ''
description?: string | React.ReactNode = ''
onBtnClick?: () => void
backUrl?: string = '/'
btnAccess?: string
showBorder?: boolean = true
className?: string = ''
contentClassName?: string = ''
headerClassName?: string = ''
/** 整个页面滚动 */
scrollPage?: boolean = true
customBtn?: ReactNode
}
const InsidePage:FC<InsidePageProps> = ({showBanner=true,pageTitle,tagList,showBtn,btnTitle,btnAccess,description,children,onBtnClick,backUrl,showBorder=true,className='',contentClassName='',headerClassName='',scrollPage=true,customBtn})=>{
const navigate = useNavigate();
const InsidePage: FC<InsidePageProps> = ({
showBanner = true,
pageTitle,
tagList,
showBtn,
btnTitle,
btnAccess,
description,
children,
onBtnClick,
backUrl,
showBorder = true,
className = '',
contentClassName = '',
headerClassName = '',
scrollPage = true,
customBtn
}) => {
const navigate = useNavigate()
const goBack = () => {
navigate(backUrl || '/');
};
return (
// <div className="h-full flex flex-col flex-1 overflow-hidden bg-[#f7f8fa]">
<div className={`h-full flex flex-col flex-1 overflow-hidden ${className}`}>
{ showBanner && <div className={`border-[0px] mr-PAGE_INSIDE_X ${showBorder ? 'border-b-[1px] border-solid border-BORDER' : ''} ${headerClassName}`}>
{!pageTitle && !description && !backUrl &&!customBtn ? <></>: <div className="mb-[30px]">
{backUrl &&<div className="text-[18px] leading-[25px] mb-[12px]">
<Button type="text" onClick={goBack}><ArrowLeftOutlined className="max-h-[14px]" />{$t('返回')}</Button>
</div>}
<div className="flex justify-between mb-[20px] items-center ">
<div className="flex items-center gap-TAG_LEFT ">
<div className="text-theme text-[26px] ">{pageTitle}</div>
{tagList && tagList?.length > 0 && tagList?.map((tag)=>{
return ( <Tag key={tag.label as string} bordered={false} >{tag.label}</Tag>)
})}
</div>
{showBtn && <WithPermission access={btnAccess}><Button type="primary" onClick={()=> {
onBtnClick&&onBtnClick()
}}>{btnTitle}</Button></WithPermission>}
{customBtn}
</div>
<div >
{description}
</div>
</div>}
</div>}
<div className={`h-full ${scrollPage ? 'overflow-hidden' : 'overflow-auto'} ${contentClassName || ''}`}>{children}</div>
const goBack = () => {
navigate(backUrl || '/')
}
return (
<div className={`flex overflow-hidden flex-col flex-1 h-full ${className}`}>
{showBanner && (
<div
className={`border-[0px] mr-PAGE_INSIDE_X ${showBorder ? 'border-solid border-b-[1px] border-BORDER' : ''} ${headerClassName}`}
>
{!pageTitle && !description && !backUrl && !customBtn ? (
<></>
) : (
<div className="mb-[30px]">
{backUrl && (
<div className="text-[18px] leading-[25px] mb-[12px]">
<Button type="text" onClick={goBack}>
<ArrowLeftOutlined className="max-h-[14px]" />
{$t('返回')}
</Button>
</div>
)}
<div className="flex justify-between mb-[20px] items-center ">
<div className="flex items-center gap-TAG_LEFT">
<div className="text-theme text-[26px] ">{pageTitle}</div>
{tagList &&
tagList?.length > 0 &&
tagList?.map((tag) => {
return (
<Tag key={tag.label as string} bordered={false}>
{tag.label}
</Tag>
)
})}
</div>
{showBtn && (
<WithPermission access={btnAccess}>
<Button
type="primary"
onClick={() => {
onBtnClick && onBtnClick()
}}
>
{btnTitle}
</Button>
</WithPermission>
)}
{customBtn}
</div>
<div>{description}</div>
</div>
)}
</div>
)
)}
<div className={`h-full ${scrollPage ? 'overflow-hidden' : 'overflow-auto'} ${contentClassName || ''}`}>
{children}
</div>
</div>
)
}
export default InsidePage
export default InsidePage
@@ -1,11 +1,11 @@
import { Dropdown, Row, Col, Button } from 'antd';
import i18n from '@common/locales';
import { memo, useEffect, useMemo } from 'react';
import { useGlobalContext } from '@common/contexts/GlobalStateContext';
import { Icon } from '@iconify/react/dist/iconify.js';
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import i18n from '@common/locales'
import { Icon } from '@iconify/react/dist/iconify.js'
import { Button, Dropdown } from 'antd'
import { memo, useEffect, useMemo } from 'react'
const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => {
const { dispatch, state } = useGlobalContext();
const { dispatch, state } = useGlobalContext()
const items = [
{
key: 'en-US',
@@ -14,7 +14,7 @@ const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => {
English
</Button>
),
title: 'English',
title: 'English'
},
{
key: 'ja-JP',
@@ -23,7 +23,7 @@ const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => {
</Button>
),
title: '日本語',
title: '日本語'
},
{
key: 'zh-TW',
@@ -32,7 +32,7 @@ const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => {
</Button>
),
title: '繁體中文',
title: '繁體中文'
},
{
key: 'zh-CN',
@@ -41,40 +41,41 @@ const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => {
</Button>
),
title: '简体中文',
},
];
title: '简体中文'
}
]
const langLabel = useMemo(
() => items.find(item => item?.key === state.language)?.title,
[state.language]
);
const langLabel = useMemo(() => items.find((item) => item?.key === state.language)?.title, [state.language])
useEffect(() => {
const savedLang = sessionStorage.getItem('i18nextLng');
const browserLang = navigator.language || navigator.userLanguage;
if (savedLang) return;
const savedLang = i18n.language || sessionStorage.getItem('i18nextLng')
if (savedLang && state.language !== savedLang) {
dispatch({ type: 'UPDATE_LANGUAGE', language: savedLang })
} else if (!savedLang) {
const browserLang = navigator.language
const supportedLang = items.find((item) => item.key === browserLang) ? browserLang : 'zh-CN'
dispatch({ type: 'UPDATE_LANGUAGE', language: supportedLang })
i18n.changeLanguage(supportedLang)
}
}, [])
dispatch({ type: 'UPDATE_LANGUAGE', language: browserLang });
}, []);
return (
<Dropdown
trigger={['hover']}
menu={{
items,
style: { minWidth: '80px' },
onClick: e => {
const { key } = e;
dispatch({ type: 'UPDATE_LANGUAGE', language: key });
i18n.changeLanguage(key);
},
onClick: (e) => {
const { key } = e
dispatch({ type: 'UPDATE_LANGUAGE', language: key })
i18n.changeLanguage(key)
sessionStorage.setItem('i18nextLng', key)
}
}}
>
<Button
className={`border-none ${
mode === 'dark'
? 'text-[#333] hover:text-[#333333b3]'
: 'text-[#ffffffb3] hover:text-[#fff] '
mode === 'dark' ? 'text-[#333] hover:text-[#333333b3]' : 'text-[#ffffffb3] hover:text-[#fff] '
}`}
type="default"
ghost
@@ -86,6 +87,6 @@ const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => {
</span>
</Button>
</Dropdown>
);
};
export default memo(LanguageSetting);
)
}
export default memo(LanguageSetting)
@@ -1,171 +1,199 @@
import { TransferProps, TreeDataNode, Tree, Spin, Input, Empty } from "antd";
import { DataNode } from "antd/es/tree";
import { Ref, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
import { ApartmentOutlined, LoadingOutlined, UserOutlined } from "@ant-design/icons";
import { ColumnsType } from "antd/es/table";
import { $t } from "@common/locales";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { TransferProps, TreeDataNode, Tree, Spin, Input, Empty } from 'antd'
import { DataNode } from 'antd/es/tree'
import { Ref, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { ApartmentOutlined, LoadingOutlined, UserOutlined } from '@ant-design/icons'
import { ColumnsType } from 'antd/es/table'
import { $t } from '@common/locales'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
export type TransferTableProps<T> = {
request?:(k?:string)=>Promise<{data:T[],success:boolean}>
request?: (k?: string) => Promise<{ data: T[]; success: boolean }>
columns: ColumnsType<T>
primaryKey:string
onSelect:(selectedData:string[])=>void
tableType?:'member'|'api'
disabledData:string[]
searchPlaceholder?:string
primaryKey: string
onSelect: (selectedData: string[]) => void
tableType?: 'member' | 'api'
disabledData: string[]
searchPlaceholder?: string
}
export type TransferTableHandle<T> = {
selectedRowKeys: () => React.Key[];
selectedRowKeys: () => React.Key[]
}
interface TreeTransferProps {
dataSource: TreeDataNode[];
targetKeys: TransferProps['targetKeys'];
onChange: TransferProps['onChange'];
dataSource: TreeDataNode[]
targetKeys: TransferProps['targetKeys']
onChange: TransferProps['onChange']
}
const generateTree = (
treeNodes: TreeDataNode[] = [],
checkedKeys: TreeTransferProps['targetKeys'] = [],
checkedKeys: TreeTransferProps['targetKeys'] = [],
filterUnchecked: boolean = false,
disabledData:string[],
filteredItems?:Set<string>
disabledData: string[],
filteredItems?: Set<string>
): TreeDataNode[] => {
const checkedKeysSet = new Set(checkedKeys);
const checkedKeysSet = new Set(checkedKeys)
return treeNodes
.map(({ children, ...props }) => {
const childNodes = generateTree(children, checkedKeys, filterUnchecked, disabledData, filteredItems);
const isDisabled = (!filterUnchecked && disabledData && disabledData.indexOf(props.id as string) !== -1)
? true
: (filterUnchecked ? false : checkedKeysSet.has(props.id as string));
const hasEnabledChild = childNodes.some(node => !node.disabled);
const childNodes = generateTree(children, checkedKeys, filterUnchecked, disabledData, filteredItems)
const isDisabled =
!filterUnchecked && disabledData && disabledData.indexOf(props.id as string) !== -1
? true
: filterUnchecked
? false
: checkedKeysSet.has(props.id as string)
const hasEnabledChild = childNodes.some((node) => !node.disabled)
return {
...props,
title: <span className="w-full truncate ml-[4px] block">{props.name}</span>,
key: props.id,
disabled: isDisabled && !hasEnabledChild,
children: childNodes,
};
})
.filter(node => {
let res:boolean= true
if(filterUnchecked){
res =(!disabledData || disabledData.indexOf(node.key as string) === -1) && (checkedKeysSet.has(node.key as string) || (node.children && node.children.length > 0) )
children: childNodes
}
if(filterUnchecked && filteredItems &&((filteredItems.size && !filteredItems.has(node.key as string))&& !(node.children && node.children.length > 0) )){
})
.filter((node) => {
let res: boolean = true
if (filterUnchecked) {
res =
(!disabledData || disabledData.indexOf(node.key as string) === -1) &&
(checkedKeysSet.has(node.key as string) || (node.children && node.children.length > 0))
}
if (
filterUnchecked &&
filteredItems &&
filteredItems.size &&
!filteredItems.has(node.key as string) &&
!(node.children && node.children.length > 0)
) {
return false
}
return res
}
)
};
return res
})
}
const MemberTransfer = forwardRef<
TransferTableHandle<{ [k: string]: unknown }>,
TransferTableProps<{ [k: string]: unknown }>
>(<T extends { [k: string]: unknown }>(props: TransferTableProps<T>, ref: Ref<TransferTableHandle<T>>) => {
const { request, columns, primaryKey, onSelect, tableType, disabledData = [], searchPlaceholder } = props
const [targetKeys, setTargetKeys] = useState<TreeTransferProps['targetKeys']>([])
const [dataSource, setDataSource] = useState<DataNode[]>([])
const parentRef = useRef<HTMLDivElement>(null)
const [loading, setLoading] = useState<boolean>(false)
const { state } = useGlobalContext()
const [expandedKeys, setExpandedKeys] = useState<string[]>([])
const [searchWord, setSearchWord] = useState<string>('')
useEffect(() => {
setTargetKeys(disabledData)
}, [disabledData])
const MemberTransfer= forwardRef<TransferTableHandle<{[k:string]:unknown}>, TransferTableProps<{[k:string]:unknown}>>(
<T extends {[k:string]:unknown}>(props: TransferTableProps<T>, ref:Ref<TransferTableHandle<T>>) => {
const {request,columns,primaryKey,onSelect,tableType,disabledData = [],searchPlaceholder} = props
const [targetKeys, setTargetKeys] = useState<TreeTransferProps['targetKeys']>([]);
const [dataSource, setDataSource] = useState<DataNode[] >([])
const parentRef = useRef<HTMLDivElement>(null);
const [loading, setLoading] = useState<boolean>(false)
const {state} = useGlobalContext()
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [searchWord, setSearchWord] = useState<string>('')
useEffect(()=>{
setTargetKeys(disabledData)
},[disabledData])
useImperativeHandle(ref, () => ({
selectedRowKeys: () => targetKeys
}))
useImperativeHandle(ref, () =>({
selectedRowKeys: () => targetKeys,}))
const translatedDataSource = useMemo(()=>{
const translatedDataSource = useMemo(() => {
const loop = (data: DataNode[]): DataNode[] =>
data?.map((item) => {
const strTitle:string = item.name === '所有成员' ? $t(item.name) as string : item.name as string;
const index = strTitle.indexOf(searchWord);
const beforeStr = strTitle.substring(0, index);
const afterStr = strTitle.slice(index + searchWord.length);
const title =
index > -1 ? (
<span className='w-[calc(100%-16px)] truncate' title={strTitle}>
{beforeStr}
<span className="text-theme">{searchWord}</span>
{afterStr}
</span>
) : (
<span className='w-[calc(100%-16px)] truncate' title={`${strTitle}`}>{strTitle}</span>
)
if (item.children) {
return {
...item,
title,
disableCheckbox:disabledData.indexOf(item.key as string) !== -1,
icon:<ApartmentOutlined />,
children: loop(item.children as T[]) };
}
const strTitle: string = item.name === '所有成员' ? ($t(item.name) as string) : (item.name as string)
const index = strTitle.indexOf(searchWord)
const beforeStr = strTitle.substring(0, index)
const afterStr = strTitle.slice(index + searchWord.length)
const title =
index > -1 ? (
<span className="w-[calc(100%-16px)] truncate" title={strTitle}>
{beforeStr}
<span className="text-theme">{searchWord}</span>
{afterStr}
</span>
) : (
<span className="w-[calc(100%-16px)] truncate" title={`${strTitle}`}>
{strTitle}
</span>
)
if (item.children) {
return {
...item,
title,
icon:<UserOutlined />,
isLeaf:true,
disableCheckbox:disabledData.indexOf(item.key as string) !== -1
};
});
return loop(dataSource);
},[dataSource, state.language, searchWord])
...item,
title,
disableCheckbox: disabledData.indexOf(item.key as string) !== -1,
icon: <ApartmentOutlined />,
children: loop(item.children as T[])
}
}
return {
...item,
title,
icon: <UserOutlined />,
isLeaf: true,
disableCheckbox: disabledData.indexOf(item.key as string) !== -1
}
})
return loop(dataSource)
}, [dataSource, state.language, searchWord])
const getInitExpandKeys = (data:T[], expandKeys:string[] = [])=>{
data.forEach((item)=>{
if(item.children?.length){
const getInitExpandKeys = (data: T[], expandKeys: string[] = []) => {
data.forEach((item) => {
if (item.children?.length) {
expandKeys.push(item.key as string)
getInitExpandKeys(item.children,expandKeys)
getInitExpandKeys(item.children, expandKeys)
}
})
return expandKeys
}
const getDataSource = ()=>{
setLoading(true)
request && request().then((res)=>{
const {data,success} = res
setDataSource(success? data : [])
setExpandedKeys(getInitExpandKeys(success? data:[]))
}).finally(()=>{setLoading(false)})
}
const getDataSource = () => {
setLoading(true)
request &&
request()
.then((res) => {
const { data, success } = res
setDataSource(success ? data : [])
setExpandedKeys(getInitExpandKeys(success ? data : []))
})
.finally(() => {
setLoading(false)
})
}
useEffect(() => {
getDataSource()
}, []);
}, [])
return (
<div ref={parentRef}>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className=''>
<Input className="mb-[10px]" placeholder={searchPlaceholder} onChange={(e)=>setSearchWord(e.target.value)} value={searchWord} />
<>{ translatedDataSource && translatedDataSource.length > 0 ? <Tree
checkable
expandedKeys={expandedKeys}
checkedKeys={targetKeys}
selectable={false}
onCheck={(e)=>{setTargetKeys(e);
onSelect(((e as string[])?.filter(x=>disabledData.indexOf(x as string) === -1))||[])}}
onExpand={setExpandedKeys}
treeData={translatedDataSource}
blockNode
showIcon
/>
: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/> }</>
</Spin>
</div>
);
return (
<div ref={parentRef}>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className="">
<Input
className="mb-[10px]"
placeholder={searchPlaceholder}
onChange={(e) => setSearchWord(e.target.value)}
value={searchWord}
/>
<>
{translatedDataSource && translatedDataSource.length > 0 ? (
<Tree
checkable
expandedKeys={expandedKeys}
checkedKeys={targetKeys}
selectable={false}
onCheck={(e) => {
setTargetKeys(e)
onSelect((e as string[])?.filter((x) => disabledData.indexOf(x as string) === -1) || [])
}}
onExpand={setExpandedKeys}
treeData={translatedDataSource}
blockNode
showIcon
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</>
</Spin>
</div>
)
})
export default MemberTransfer;
export default MemberTransfer
@@ -1,22 +1,22 @@
import React, { useEffect, useState } from 'react';
import { Result, Skeleton } from 'antd';
import React, { useEffect, useState } from 'react'
import { Result, Skeleton } from 'antd'
const NotFound: React.FC = () => {
const [showPage, setShowPage] = useState<boolean>(false)
const [showPage, setShowPage] = useState<boolean>(false)
useEffect(()=>{
setTimeout(()=>setShowPage(true), 1000)
},[])
useEffect(() => {
setTimeout(() => setShowPage(true), 1000)
}, [])
return (
<div className={`h-full w-full flex flex-1 align-middle ${showPage ? 'items-center' : ''}`}>
{ showPage ? <Result
className='w-full'
status="404"
title="404"
subTitle="Sorry, the page you visited does not exist."
/> : <Skeleton active /> }
</div>
)}
return (
<div className={`h-full w-full flex flex-1 align-middle ${showPage ? 'items-center' : ''}`}>
{showPage ? (
<Result className="w-full" status="404" title="404" subTitle="Sorry, the page you visited does not exist." />
) : (
<Skeleton active />
)}
</div>
)
}
export default NotFound;
export default NotFound
@@ -1,243 +1,370 @@
import {Button, Dropdown, Input, MenuProps, TablePaginationConfig} from 'antd';
import {ChangeEvent, RefAttributes, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {ActionType, ParamsType, ProColumns, ProTableProps} from '@ant-design/pro-components';
import {
DragSortTable,
ProTable,
} from '@ant-design/pro-components';
import './PageList.module.css'
import {SearchOutlined} from "@ant-design/icons";
import { SearchOutlined } from '@ant-design/icons'
import type { ActionType, ParamsType, ProColumns, ProTableProps } from '@ant-design/pro-components'
import { DragSortTable, ProTable } from '@ant-design/pro-components'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { PERMISSION_DEFINITION } from '@common/const/permissions'
import { withMinimumDelay } from '@common/utils/ux'
import { Button, Dropdown, Input, MenuProps, TablePaginationConfig } from 'antd'
import { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface'
import { debounce } from 'lodash-es'
import WithPermission from '@common/components/aoplatform/WithPermission';
import { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface';
import { useGlobalContext } from '../../contexts/GlobalStateContext';
import { PERMISSION_DEFINITION } from '@common/const/permissions';
import { withMinimumDelay } from '@common/utils/ux';
import {
ChangeEvent,
RefAttributes,
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState
} from 'react'
import { useGlobalContext } from '../../contexts/GlobalStateContext'
import './PageList.module.css'
export type PageProColumns<T = any, ValueType = 'text'> = ProColumns<T , ValueType> & {btnNums? : number}
export type PageProColumns<T = any, ValueType = 'text'> = ProColumns<T, ValueType> & { btnNums?: number }
interface PageListProps<T> extends ProTableProps<T, unknown>, RefAttributes<ActionType> {
id?:string
columns: PageProColumns<T,'text'>[]
request?:(params: (ParamsType & {pageSize?: number | undefined, current?: number | undefined, keyword?: string | undefined}), sorter: unknown, filter: unknown)=>Promise<{data:T[], success:boolean}>
dropMenu?:MenuProps
searchPlaceholder?:string
showPagination?:boolean
primaryKey?:string
addNewBtnTitle?:string
addNewBtnAccess?:string
tableClickAccess?:string
onAddNewBtnClick?:()=>void
beforeSearchNode?:React.ReactNode[]
onSearchWordChange?:(e:ChangeEvent<HTMLInputElement>) => void
afterNewBtn?:React.ReactNode[]
dragSortKey?:string
onDragSortEnd?:(beforeIndex: number, afterIndex: number, newDataSource: T[]) => void | Promise<void>
tableTitle?:string
dataSource?:T[]
onRowClick?:(record:T)=>void
showColSetting?:boolean
minVirtualHeight?:number
besidesTableHeight?:number
noTop?:boolean
tableClass?:string
tableTitleClass?:string
addNewBtnWrapperClass?:string
delayLoading?:boolean
noScroll?:boolean
interface PageListProps<T> extends ProTableProps<T, unknown>, RefAttributes<ActionType> {
id?: string
columns: PageProColumns<T, 'text'>[]
request?: (
params: ParamsType & { pageSize?: number | undefined; current?: number | undefined; keyword?: string | undefined },
sorter: unknown,
filter: unknown
) => Promise<{ data: T[]; success: boolean }>
dropMenu?: MenuProps
searchPlaceholder?: string
showPagination?: boolean
primaryKey?: string
addNewBtnTitle?: string
addNewBtnAccess?: string
tableClickAccess?: string
onAddNewBtnClick?: () => void
beforeSearchNode?: React.ReactNode[]
onSearchWordChange?: (e: ChangeEvent<HTMLInputElement>) => void
afterNewBtn?: React.ReactNode[]
dragSortKey?: string
onDragSortEnd?: (beforeIndex: number, afterIndex: number, newDataSource: T[]) => void | Promise<void>
tableTitle?: string
dataSource?: T[]
onRowClick?: (record: T) => void
showColSetting?: boolean
minVirtualHeight?: number
besidesTableHeight?: number
noTop?: boolean
tableClass?: string
tableTitleClass?: string
addNewBtnWrapperClass?: string
delayLoading?: boolean
noScroll?: boolean
/* 前端分页的表格,需要传入该字段以支持后端搜索 */
manualReloadTable?:()=>void
manualReloadTable?: () => void
}
const PageList = <T extends Record<string, unknown>>(props: React.PropsWithChildren<PageListProps<T>>,ref: React.Ref<ActionType>) => {
const {id,columns,request,dropMenu,searchPlaceholder,showPagination=true,primaryKey='id',addNewBtnTitle,addNewBtnAccess,tableClickAccess,tableClass,onAddNewBtnClick,beforeSearchNode,onSearchWordChange,manualReloadTable,afterNewBtn,dragSortKey,onDragSortEnd,tableTitle,rowSelection,onChange,dataSource,onRowClick,showColSetting=false,minVirtualHeight,noTop,addNewBtnWrapperClass = '',tableTitleClass,delayLoading = true,besidesTableHeight, noScroll} = props
const parentRef = useRef<HTMLDivElement>(null);
const [tableHeight, setTableHeight] = useState(minVirtualHeight || window.innerHeight);
const [tableWidth, setTableWidth] = useState<number|undefined>(undefined);
const actionRef = useRef<ActionType>();
const [allowTableClick,setAllowTableClick] = useState<boolean>(false)
const {accessData,checkPermission,accessInit,state} = useGlobalContext()
const PageList = <T extends Record<string, unknown>>(
props: React.PropsWithChildren<PageListProps<T>>,
ref: React.Ref<ActionType>
) => {
const {
id,
columns,
request,
dropMenu,
searchPlaceholder,
showPagination = true,
primaryKey = 'id',
addNewBtnTitle,
addNewBtnAccess,
tableClickAccess,
tableClass,
onAddNewBtnClick,
beforeSearchNode,
onSearchWordChange,
manualReloadTable,
afterNewBtn,
dragSortKey,
onDragSortEnd,
tableTitle,
rowSelection,
onChange,
dataSource,
onRowClick,
showColSetting = false,
minVirtualHeight,
noTop,
addNewBtnWrapperClass = '',
tableTitleClass,
delayLoading = true,
besidesTableHeight,
noScroll
} = props
const parentRef = useRef<HTMLDivElement>(null)
const [tableHeight, setTableHeight] = useState(minVirtualHeight || window.innerHeight)
const [tableWidth, setTableWidth] = useState<number | undefined>(undefined)
const actionRef = useRef<ActionType>()
const [allowTableClick, setAllowTableClick] = useState<boolean>(false)
const { accessData, checkPermission, accessInit, state } = useGlobalContext()
const [minTableWidth, setMinTableWidth] = useState<number>(0)
// 使用useImperativeHandle来自定义暴露给父组件的实例值
useImperativeHandle(ref, () => actionRef.current!);
useImperativeHandle(ref, () => actionRef.current!)
useEffect(()=>{
useEffect(() => {
actionRef?.current?.reload?.()
},[state.language])
const lastAccess = useMemo(()=>{
if(!tableClickAccess) return true
return checkPermission(tableClickAccess as keyof typeof PERMISSION_DEFINITION[0])
},[allowTableClick, accessData,accessInit])
}, [state.language])
useEffect(()=>{
tableClickAccess ? setAllowTableClick(lastAccess) : setAllowTableClick(true)
},[accessData])
const resizeObserverRef = useRef<ResizeObserver |null >(null);
const lastAccess = useMemo(() => {
if (!tableClickAccess) return true
return checkPermission(tableClickAccess as keyof (typeof PERMISSION_DEFINITION)[0])
}, [allowTableClick, accessData, accessInit])
useEffect(() => {
tableClickAccess ? setAllowTableClick(lastAccess) : setAllowTableClick(true)
}, [accessData])
const resizeObserverRef = useRef<ResizeObserver | null>(null)
useEffect(() => {
const handleResize = () => {
if (parentRef.current && !noScroll) {
const res = parentRef.current.getBoundingClientRect();
const height = res.height - ((noTop ? 0 : 59) + 54 + (showPagination && !dragSortKey ? 52 : 0) +( besidesTableHeight ?? 0) + 1); // 减去顶部按钮、底部分页、表头高度
setTableWidth(minTableWidth - 5> res.width ? minTableWidth : undefined);
height && setTableHeight(minVirtualHeight === undefined ? height : (height > minVirtualHeight ? height : minVirtualHeight));
const res = parentRef.current.getBoundingClientRect()
const height =
res.height -
((noTop ? 0 : 59) + 54 + (showPagination && !dragSortKey ? 52 : 0) + (besidesTableHeight ?? 0) + 1) // 减去顶部按钮、底部分页、表头高度
setTableWidth(minTableWidth - 5 > res.width ? minTableWidth : undefined)
height &&
setTableHeight(
minVirtualHeight === undefined ? height : height > minVirtualHeight ? height : minVirtualHeight
)
}
};
}
const debouncedHandleResize = debounce(handleResize, 200);
const debouncedHandleResize = debounce(handleResize, 200)
if (!resizeObserverRef.current && !noScroll) {
// 创建一个 ResizeObserver 来监听高度变化,只创建一次
resizeObserverRef.current = new ResizeObserver(debouncedHandleResize);
resizeObserverRef.current = new ResizeObserver(debouncedHandleResize)
// 开始监听
if (parentRef.current && !minVirtualHeight) {
resizeObserverRef.current.observe(parentRef.current);
resizeObserverRef.current.observe(parentRef.current)
}
}
// 在 minTableWidth 变化时手动触发 handleResize
handleResize();
handleResize()
// 清理函数
return () => {
if (resizeObserverRef.current) {
resizeObserverRef.current.disconnect();
resizeObserverRef.current = null;
resizeObserverRef.current.disconnect()
resizeObserverRef.current = null
}
};
}, [minTableWidth, parentRef, noTop, showPagination, dragSortKey, minVirtualHeight]); // 将相关依赖项作为 useEffect 的依赖项
}
}, [minTableWidth, parentRef, noTop, showPagination, dragSortKey, minVirtualHeight]) // 将相关依赖项作为 useEffect 的依赖项
const newColumns = useMemo(()=>{
let width:number = 0
const res = columns?.map(
(x, index)=>{
const sorter = localStorage.getItem(`${id}_sorter`)
const filters = localStorage.getItem(`${id}_filters`)
x.copyable = x.copyable ?? (index === 0 || x.dataIndex === 'id' || x.dataIndex === 'email')
if(sorter && x.sorter){
const sorterObj = JSON.parse(sorter)
const xName = Array.isArray(x.dataIndex) ? x.dataIndex.join(','):x.dataIndex
x.defaultSortOrder = sorterObj?.columnKey === xName ? sorterObj?.order : undefined
// x.showSorterTooltip = {target:'sorter-icon'}
}
if(filters && x.filters){
const filtersObj = JSON.parse(filters)
const xName = Array.isArray(x.dataIndex) ? x.dataIndex.join(','):x.dataIndex
x.defaultFilteredValue = filtersObj?.[xName as string]
}
if((index === columns.length -1 || x.key === 'option') && x.btnNums){
const optionWidth = 24 + 18 * x.btnNums + (x.btnNums - 1) * 21
x.width = Math.max(optionWidth, 54)
}
width += Number(x.width ?? ((x.filters || x.sorter) ? 120 : 100))
return x})
setMinTableWidth(width)
const newColumns = useMemo(() => {
let width: number = 0
const res = columns?.map((x, index) => {
const sorter = localStorage.getItem(`${id}_sorter`)
const filters = localStorage.getItem(`${id}_filters`)
x.copyable = x.copyable ?? (index === 0 || x.dataIndex === 'id' || x.dataIndex === 'email')
if (sorter && x.sorter) {
const sorterObj = JSON.parse(sorter)
const xName = Array.isArray(x.dataIndex) ? x.dataIndex.join(',') : x.dataIndex
x.defaultSortOrder = sorterObj?.columnKey === xName ? sorterObj?.order : undefined
// x.showSorterTooltip = {target:'sorter-icon'}
}
if (filters && x.filters) {
const filtersObj = JSON.parse(filters)
const xName = Array.isArray(x.dataIndex) ? x.dataIndex.join(',') : x.dataIndex
x.defaultFilteredValue = filtersObj?.[xName as string]
}
if ((index === columns.length - 1 || x.key === 'option') && x.btnNums) {
const optionWidth = 24 + 18 * x.btnNums + (x.btnNums - 1) * 21
x.width = Math.max(optionWidth, 54)
}
width += Number(x.width ?? (x.filters || x.sorter ? 120 : 100))
return x
})
setMinTableWidth(width)
return res
},[columns])
}, [columns])
const headerTitle = ()=>{
const headerTitle = () => {
return (
<>{
tableTitle ? <span className={`text-[30px] leading-[42px] my-mbase pl-[20px] ${tableTitleClass}`}>{tableTitle}</span> : (
addNewBtnTitle ? <WithPermission access={addNewBtnAccess} ><Button type="primary" className={`mr-btnrbase my-btnbase ${addNewBtnWrapperClass}`} onClick={onAddNewBtnClick}>{addNewBtnTitle}</Button></WithPermission> : undefined
)
}
{afterNewBtn ? afterNewBtn as React.ReactNode[] :undefined}
</>
)
<>
{tableTitle ? (
<span className={`text-[30px] leading-[42px] my-mbase pl-[20px] ${tableTitleClass}`}>{tableTitle}</span>
) : addNewBtnTitle ? (
<WithPermission access={addNewBtnAccess}>
<Button
type="primary"
className={`mr-btnrbase my-btnbase ${addNewBtnWrapperClass}`}
onClick={onAddNewBtnClick}
>
{addNewBtnTitle}
</Button>
</WithPermission>
) : undefined}
{afterNewBtn ? (afterNewBtn as React.ReactNode[]) : undefined}
</>
)
}
const requestWithDelay = (params: ParamsType & { pageSize?: number | undefined; current?: number | undefined; keyword?: string | undefined;}, sort: unknown, filter: unknown) => {
return withMinimumDelay(() => request!(params, sort, filter), delayLoading === false? 0 : undefined);
};
const getTableActions = () => {
return [
...(beforeSearchNode ? [beforeSearchNode] : []),
...(searchPlaceholder
? [
<Input
key="search-input"
className="my-btnbase ml-btnbase"
onChange={onSearchWordChange ? (e) => debounce(onSearchWordChange, 100)(e) : undefined}
onPressEnter={() => {
if (manualReloadTable) {
manualReloadTable()
return
}
if (actionRef.current) {
actionRef.current.reset?.()
actionRef.current.reload?.()
}
}}
allowClear
placeholder={searchPlaceholder}
prefix={
<SearchOutlined
className="cursor-pointer"
onClick={() => {
if (actionRef.current) {
actionRef.current.reset?.()
actionRef.current.reload?.()
}
}}
/>
}
/>
]
: [])
]
}
const requestWithDelay = (
params: ParamsType & { pageSize?: number | undefined; current?: number | undefined; keyword?: string | undefined },
sort: unknown,
filter: unknown
) => {
return withMinimumDelay(() => request!(params, sort, filter), delayLoading === false ? 0 : undefined)
}
return (
<div ref={parentRef} className={`eo_page_list bg-MAIN_BG ${dragSortKey ? 'eo_page_drag':''} ${tableClass ?? ''}`}style={{ height: '100%' }}>
{dragSortKey? <DragSortTable<T>
<div
ref={parentRef}
className={`eo_page_list bg-MAIN_BG ${dragSortKey ? 'eo_page_drag' : ''} ${tableClass ?? ''}`}
style={{ height: '100%' }}
>
{dragSortKey ? (
<DragSortTable<T>
actionRef={actionRef}
columns={newColumns}
rowKey={primaryKey}
search={false}
pagination={false}
pagination={
showPagination
? {
showSizeChanger: true,
showQuickJumper: true,
size: 'default'
}
: false
}
request={request}
dragSortKey={dragSortKey}
onDragSortEnd={onDragSortEnd}
scroll={noScroll ? undefined :{ y: tableHeight }}
scroll={noScroll ? undefined : { y: tableHeight }}
options={{
reload: false,
density: false,
setting: false,
setting: false
}}
headerTitle={
headerTitle()
}
/> : <ProTable<T>
toolbar={{
actions: getTableActions()
}}
headerTitle={headerTitle()}
/>
) : (
<ProTable<T>
actionRef={actionRef}
columns={newColumns}
virtual
scroll={noScroll ? undefined : {x:tableWidth,y: tableHeight }}
scroll={noScroll ? undefined : { x: tableWidth, y: tableHeight }}
size="middle"
rowSelection={rowSelection}
tableAlertRender={false}
tableAlertOptionRender={false}
request={request ? requestWithDelay : undefined}
toolBarRender={() => [
dropMenu ? (<Dropdown
key="menu"
menu={dropMenu}
>
<Button>
</Button>
</Dropdown>):null,
dropMenu ? (
<Dropdown key="menu" menu={dropMenu}>
<Button></Button>
</Dropdown>
) : null
]}
toolbar={{
actions:[...[beforeSearchNode],...[searchPlaceholder?<Input className="my-btnbase ml-btnbase" onChange={ onSearchWordChange ? (e) => debounce(onSearchWordChange, 100)(e) : undefined } onPressEnter={()=>manualReloadTable ? manualReloadTable():actionRef.current?.reload?.()} allowClear placeholder={searchPlaceholder} prefix={<SearchOutlined className="cursor-pointer" onClick={()=>{actionRef.current?.reload?.()}}/>}/>:null]],
actions: getTableActions()
}}
options={{
reload: false,
density: false,
setting: showColSetting ? {
draggable:false,
showListItemOption:false
} :false,
setting: showColSetting
? {
draggable: false,
showListItemOption: false
}
: false
}}
showSorterTooltip={false}
columnsState={{persistenceType:'localStorage',persistenceKey:id}}
pagination={showPagination ? {
showSizeChanger: true,
showQuickJumper: true,
size:'default'
}:false}
columnsState={{ persistenceType: 'localStorage', persistenceKey: id }}
pagination={
showPagination
? {
showSizeChanger: true,
showQuickJumper: true,
size: 'default'
}
: false
}
onChange={(
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<T> | SorterResult<T>[],
extra: TableCurrentDataSource<T>
) => {
localStorage.setItem(`${id}_filters`, JSON.stringify(filters))
!Array.isArray(sorter) &&
localStorage.setItem(
`${id}_sorter`,
JSON.stringify({ columnKey: sorter?.columnKey, order: sorter?.order })
)
onChange?.(pagination, filters, sorter, extra)
}}
rowKey={primaryKey}
onChange={(pagination: TablePaginationConfig, filters: Record<string, FilterValue | null>, sorter: SorterResult<T> | SorterResult<T>[],extra:TableCurrentDataSource<T>) =>{
localStorage.setItem(`${id}_filters`,JSON.stringify(filters))
!Array.isArray(sorter) && localStorage.setItem(`${id}_sorter`,JSON.stringify({columnKey:sorter?.columnKey, order: sorter?.order}))
onChange?.(pagination,filters,sorter,extra)}}
dataSource={dataSource}
search={false}
headerTitle={
headerTitle()
headerTitle={headerTitle()}
onRow={
onRowClick && allowTableClick
? (record) => ({
onClick: () => {
onRowClick(record)
}
})
: undefined
}
onRow={onRowClick && allowTableClick ? (record) => ({
onClick: () => {
onRowClick(record);
}
}):undefined}
rowClassName={()=>onRowClick && allowTableClick ?"cursor-pointer":''}
/>}
rowClassName={() => (onRowClick && allowTableClick ? 'cursor-pointer' : '')}
/>
)}
</div>
);
};
)
}
export default forwardRef(PageList) as <T extends Record<string,unknown>>(props: React.PropsWithChildren<PageListProps<T>> & { ref?: React.Ref<ActionType> }) => ReturnType<typeof PageList>;
export default forwardRef(PageList) as <T extends Record<string, unknown>>(
props: React.PropsWithChildren<PageListProps<T>> & { ref?: React.Ref<ActionType> }
) => ReturnType<typeof PageList>
@@ -1,95 +1,100 @@
import {App, Form, Input, Row, Table} from "antd";
import {forwardRef, useEffect, useImperativeHandle, useMemo} from "react";
import {useFetch} from "@common/hooks/http.ts";
import {BasicResponse, PLACEHOLDER, PolicyPublishColumns, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { $t } from "@common/locales";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { PolicyPublishModalHandle, PolicyPublishModalProps } from "@common/const/type";
import { App, Form, Input, Row, Table } from 'antd'
import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react'
import { useFetch } from '@common/hooks/http.ts'
import { BasicResponse, PLACEHOLDER, PolicyPublishColumns, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { $t } from '@common/locales'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { PolicyPublishModalHandle, PolicyPublishModalProps } from '@common/const/type'
export const PolicyPublishModalContent = forwardRef<PolicyPublishModalHandle, PolicyPublishModalProps>((props, ref) => {
const { message } = App.useApp()
const { data } = props
const [form] = Form.useForm()
const { fetchData } = useFetch()
const { state } = useGlobalContext()
export const PolicyPublishModalContent = forwardRef<PolicyPublishModalHandle,PolicyPublishModalProps>((props, ref) => {
const { message } = App.useApp()
const { data} = props
const [form] = Form.useForm();
const {fetchData} = useFetch()
const {state} = useGlobalContext()
const publish:()=>Promise<boolean | string | Record<string, unknown>> = ()=>{
return new Promise((resolve, reject)=>{
form.validateFields().then((value)=>{
const body = {...value, source:data.source}
fetchData<BasicResponse<null>>('strategy/global/data-masking/publish',{method: 'POST',eoBody:body,eoTransformKeys:['versionName']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(response)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo)=> reject(errorInfo))
})
}
useImperativeHandle(ref, ()=>({
publish,
const publish: () => Promise<boolean | string | Record<string, unknown>> = () => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then((value) => {
const body = { ...value, source: data.source }
fetchData<BasicResponse<null>>('strategy/global/data-masking/publish', {
method: 'POST',
eoBody: body,
eoTransformKeys: ['versionName']
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(response)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
)
.catch((errorInfo) => reject(errorInfo))
})
}
useEffect(()=>{
form.setFieldsValue(data)
},[data])
useImperativeHandle(ref, () => ({
publish
}))
const translatedPolicyColumns = useMemo(()=>PolicyPublishColumns.map((x)=>({
...x,
title: typeof x.title === 'string' ? $t(x.title) : x.title,
})),[state.language])
useEffect(() => {
form.setFieldsValue(data)
}, [data])
const translatedPolicyColumns = useMemo(
() =>
PolicyPublishColumns.map((x) => ({
...x,
title: typeof x.title === 'string' ? $t(x.title) : x.title
})),
[state.language]
)
return (
<>
<WithPermission access=""><Form
className=" mx-auto"
form={form}
labelAlign='left'
layout='vertical'
scrollToFirstError
name="publishApprovalModalContent"
// labelCol={{span: 3}}
// wrapperCol={{span: 21}}
autoComplete="off"
>
return (
<>
<WithPermission access="">
<Form
className=" mx-auto"
form={form}
labelAlign="left"
layout="vertical"
scrollToFirstError
name="publishApprovalModalContent"
// labelCol={{span: 3}}
// wrapperCol={{span: 21}}
autoComplete="off"
>
<Form.Item label={$t('发布名称')} name="versionName" rules={[{ required: true, whitespace: true }]}>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Form.Item
label={$t("发布名称")}
name='versionName'
rules={[{required: true,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Form.Item
label={$t("描述")}
name="desc"
>
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('策略列表')}</span></Row>
<Row className="mb-mbase ">
<Table
columns={translatedPolicyColumns}
bordered={true}
rowKey="name"
size="small"
dataSource={data.strategies || []}
pagination={false}
/>
{!data?.isPublish&& data?.unpublishMsg&& <p className="text-status_fail mt-[4px]">{data.unpublishMsg}</p>}
</Row>
</Form>
</WithPermission>
</>)
})
<Form.Item label={$t('描述')} name="desc">
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Row className="mt-mbase pb-[8px] h-[32px] font-bold">
<span>{$t('策略列表')}</span>
</Row>
<Row className="mb-mbase ">
<Table
columns={translatedPolicyColumns}
bordered={true}
rowKey="name"
size="small"
dataSource={data.strategies || []}
pagination={false}
/>
{!data?.isPublish && data?.unpublishMsg && <p className="text-status_fail mt-[4px]">{data.unpublishMsg}</p>}
</Row>
</Form>
</WithPermission>
</>
)
})
@@ -1,257 +1,402 @@
import {App, Col, Form, Input, Row, Table, Tooltip} from "antd";
import {forwardRef, useEffect, useImperativeHandle, useMemo} from "react";
import {PublishApprovalInfoType, PublishApprovalModalHandle, PublishApprovalModalProps, PublishVersionTableListItem} from "@common/const/approval/type.tsx";
import {useFetch} from "@common/hooks/http.ts";
import {BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, STATUS_COLOR} from "@common/const/const.tsx";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { SYSTEM_PUBLISH_ONLINE_COLUMNS } from "@core/const/system/const.tsx";
import { $t } from "@common/locales";
import { ApprovalPolicyColumns, ApprovalRouteColumns, ApprovalStatusColorClass, ApprovalUpstreamColumns, ChangeTypeEnum } from "@common/const/approval/const";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { LoadingOutlined } from "@ant-design/icons";
import { SystemInsidePublishOnlineItems } from "@core/pages/system/publish/SystemInsidePublishOnline";
import { App, Col, Form, Input, Row, Table, Tooltip } from 'antd'
import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react'
import {
PublishApprovalInfoType,
PublishApprovalModalHandle,
PublishApprovalModalProps,
PublishVersionTableListItem
} from '@common/const/approval/type.tsx'
import { useFetch } from '@common/hooks/http.ts'
import {
BasicResponse,
FORM_ERROR_TIPS,
PLACEHOLDER,
RESPONSE_TIPS,
STATUS_CODE,
STATUS_COLOR
} from '@common/const/const.tsx'
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { SYSTEM_PUBLISH_ONLINE_COLUMNS } from '@core/const/system/const.tsx'
import { $t } from '@common/locales'
import {
ApprovalPolicyColumns,
ApprovalRouteColumns,
ApprovalStatusColorClass,
ApprovalUpstreamColumns,
ChangeTypeEnum
} from '@common/const/approval/const'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { LoadingOutlined } from '@ant-design/icons'
import { SystemInsidePublishOnlineItems } from '@core/pages/system/publish/SystemInsidePublishOnline'
export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle,PublishApprovalModalProps>((props, ref) => {
export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle, PublishApprovalModalProps>(
(props, ref) => {
const { message } = App.useApp()
const { type,data,insidePage = false, serviceType = 'rest', serviceId, teamId} = props
const [form] = Form.useForm();
const {fetchData} = useFetch()
const {state} = useGlobalContext()
const { type, data, insidePage = false, serviceType = 'rest', serviceId, teamId } = props
const [form] = Form.useForm()
const { fetchData } = useFetch()
const { state } = useGlobalContext()
const save:(operate:'pass'|'refuse')=>Promise<boolean | string> = (operate)=>{
if(type === 'view'){
const save: (operate: 'pass' | 'refuse') => Promise<boolean | string> = (operate) => {
if (type === 'view') {
return Promise.resolve(true)
}
return form
.validateFields()
.then((value) => {
if (operate === 'refuse' && form.getFieldValue('opinion') === '') {
form.setFields([
{
name: 'opinion',
errors: [$t(FORM_ERROR_TIPS.refuseOpinion)]
}
])
form.scrollToField('opinion')
return Promise.reject($t(RESPONSE_TIPS.refuseOpinion))
}
return fetchData<BasicResponse<null>>(`service/publish/${operate === 'pass' ? 'accept' : 'refuse'}`, {
method: 'PUT',
eoBody: { comments: value.opinion },
eoParams: { id: data!.id, project: serviceId },
eoTransformKeys: ['versionRemark']
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
return Promise.resolve(true)
}
return form.validateFields().then((value)=>{
if(operate === 'refuse' && form.getFieldValue('opinion') === '' ){
form.setFields([{
name:'opinion',errors:[$t(FORM_ERROR_TIPS.refuseOpinion)]
}])
form.scrollToField('opinion')
return Promise.reject($t(RESPONSE_TIPS.refuseOpinion))
}
return fetchData<BasicResponse<null>>(`service/publish/${operate === 'pass' ? 'accept' : 'refuse'}`,{method: 'PUT',eoBody:({comments:value.opinion}), eoParams:{id:data!.id, project:serviceId},eoTransformKeys:['versionRemark']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
return Promise.resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> Promise.reject(errorInfo))
}).catch((err)=> {form.scrollToField(err.errorFields[0].name[0]); return Promise.reject(err)})
}
const publish:(notSave?:boolean)=>Promise<boolean | string | Record<string, unknown>> = (notSave)=>{
return new Promise((resolve, reject)=>{
form.validateFields().then((value)=>{
const body = {...value, ...(type === 'publish'&&{release:data.id})}
fetchData<BasicResponse<null>>(
notSave ? 'service/publish/apply' : 'service/publish/release/do',{method: 'POST',eoBody:body, eoParams:{service:serviceId, team:teamId},eoTransformKeys:['versionRemark']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(response)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo)=> reject(errorInfo))
})
}
const online:()=>Promise<boolean | string> = ()=>{
return new Promise((resolve, reject)=>{
form.validateFields().then(()=>{
fetchData<BasicResponse<null>>('service/publish/execute',{method: 'PUT', eoParams:{project:serviceId,id:(data as PublishVersionTableListItem).flowId},eoTransformKeys:['versionRemark']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo)=> reject(errorInfo))
})
}
useImperativeHandle(ref, ()=>({
save,
publish,
online
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => Promise.reject(errorInfo))
})
.catch((err) => {
form.scrollToField(err.errorFields[0].name[0])
return Promise.reject(err)
})
}
const publish: (notSave?: boolean) => Promise<boolean | string | Record<string, unknown>> = (notSave) => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then((value) => {
const body = { ...value, ...(type === 'publish' && { release: data.id }) }
fetchData<BasicResponse<null>>(notSave ? 'service/publish/apply' : 'service/publish/release/do', {
method: 'POST',
eoBody: body,
eoParams: { service: serviceId, team: teamId },
eoTransformKeys: ['versionRemark']
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(response)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo) => reject(errorInfo))
})
}
const online: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then(() => {
fetchData<BasicResponse<null>>('service/publish/execute', {
method: 'PUT',
eoParams: { project: serviceId, id: (data as PublishVersionTableListItem).flowId },
eoTransformKeys: ['versionRemark']
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo) => reject(errorInfo))
})
}
useImperativeHandle(ref, () => ({
save,
publish,
online
}))
useEffect(() => {
form.setFieldsValue({ opinion: '', ...data })
}, [])
const translatedUpstreamColumns = useMemo(
() =>
ApprovalUpstreamColumns.map((x) => ({
...x,
...(x.dataIndex === 'type'
? {
valueEnum: {
static: {
text: $t('静态上游')
}
}
}
: {}),
...(x.dataIndex === 'change'
? {
render: (_, entity) => (
<Tooltip
placement="top"
title={
entity.change === 'error'
? $t('该 API 缺失(0)(1)(2)请先补充', [
entity.proxyStatus == 1 && $t('转发信息,'),
entity.docStatus == 1 && $t('文档信息,'),
entity.upstreamStatus == 1 && $t('上游信息,')
])
: ''
}
>
<span
className={`${ApprovalStatusColorClass[entity.change as keyof typeof ApprovalStatusColorClass]} truncate block`}
>
{$t(ChangeTypeEnum[entity.change as keyof typeof ChangeTypeEnum] || '-')}
{entity.change === 'error'
? $t('该 API 缺失(0)(1)(2)请先补充', [
entity.proxyStatus == 1 && $t('转发信息,'),
entity.docStatus == 1 && $t('文档信息,'),
entity.upstreamStatus == 1 && $t('上游信息,')
])
: ''}
</span>
</Tooltip>
)
}
: {}),
title: typeof x.title === 'string' ? $t(x.title) : x.title
})),
[state.language]
)
useEffect(()=>{
form.setFieldsValue({ opinion:'',...data})
},[])
const translatedUpstreamColumns = useMemo(()=>ApprovalUpstreamColumns.map((x)=>({
...x,
...(x.dataIndex === 'type' ? {valueEnum:{
'static':{
text:$t('静态上游')
}
}}:{}),
...(x.dataIndex === 'change' ? {
render:(_,entity)=>(
<Tooltip placement="top" title={entity.change === 'error' ? $t('该 API 缺失(0)(1)(2)请先补充',[entity.proxyStatus == 1 && $t('转发信息,'),entity.docStatus == 1 && $t('文档信息,'),entity.upstreamStatus == 1 && $t('上游信息,')]):''}>
<span className={`${ApprovalStatusColorClass[entity.change as keyof typeof ApprovalStatusColorClass]} truncate block`}>{$t(ChangeTypeEnum[entity.change as (keyof typeof ChangeTypeEnum)] || '-')}
{entity.change === 'error' ?$t('该 API 缺失(0)(1)(2)请先补充',[entity.proxyStatus == 1 && $t('转发信息,'),entity.docStatus == 1 && $t('文档信息,'),entity.upstreamStatus == 1 && $t('上游信息,')]):''}</span>
</Tooltip>)
}:{}),
title: typeof x.title === 'string' ? $t(x.title) : x.title,
})),[state.language])
const translatedRouteColumns = useMemo(()=>ApprovalRouteColumns.filter(x=> serviceType === 'rest' ? x.dataIndex !== 'name' : x.dataIndex !== 'methods').map((x)=>({
...x,
...(x.dataIndex === 'change' ? {
render:(_,entity)=>(
<Tooltip placement="top" title={entity.change === 'error' ?$t('该 API 缺失(0)(1)(2)请先补充',[entity.proxyStatus == 1 && $t('转发信息,'),entity.docStatus == 1 && $t('文档信息,'),entity.upstreamStatus == 1 && $t('上游信息,')]):''}>
<span className={`${ApprovalStatusColorClass[entity.change as keyof typeof ApprovalStatusColorClass]} truncate block`}>
{$t(ChangeTypeEnum[entity.change as (keyof typeof ChangeTypeEnum)] || '-')}
{entity.change === 'error' ?$t('该 API 缺失(0)(1)(2)请先补充',[entity.proxyStatus == 1 && $t('转发信息,'),entity.docStatus == 1 && $t('文档信息,'),entity.upstreamStatus == 1 && $t('上游信息,')]):''}
</span>
</Tooltip>)
}:{}
),
title: typeof x.title === 'string' ? $t(x.title) : x.title,
})),[state.language, serviceType])
const translatedPublishColumns = useMemo(()=>SYSTEM_PUBLISH_ONLINE_COLUMNS.map((x)=>{
if(x.dataIndex === 'status'){
return {...x,title:$t(x.title),
render:(_:unknown,entity:SystemInsidePublishOnlineItems)=>{
switch(entity.status){
case 'done':
return <span className={STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]}>{$t('成功')}</span>
case 'error':
return <Tooltip title={entity.error || $t('上线失败')}><span className={`${STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]} truncate block`}>{$t('失败')} {entity.error}</span></Tooltip>
default:
return <LoadingOutlined className="text-theme" spin />
const translatedRouteColumns = useMemo(
() =>
ApprovalRouteColumns.filter((x) =>
serviceType === 'rest' ? x.dataIndex !== 'name' : x.dataIndex !== 'methods'
).map((x) => ({
...x,
...(x.dataIndex === 'change'
? {
render: (_, entity) => (
<Tooltip
placement="top"
title={
entity.change === 'error'
? $t('该 API 缺失(0)(1)(2)请先补充', [
entity.proxyStatus == 1 && $t('转发信息,'),
entity.docStatus == 1 && $t('文档信息,'),
entity.upstreamStatus == 1 && $t('上游信息,')
])
: ''
}
}}
}
}),[state.language])
>
<span
className={`${ApprovalStatusColorClass[entity.change as keyof typeof ApprovalStatusColorClass]} truncate block`}
>
{$t(ChangeTypeEnum[entity.change as keyof typeof ChangeTypeEnum] || '-')}
{entity.change === 'error'
? $t('该 API 缺失(0)(1)(2)请先补充', [
entity.proxyStatus == 1 && $t('转发信息,'),
entity.docStatus == 1 && $t('文档信息,'),
entity.upstreamStatus == 1 && $t('上游信息,')
])
: ''}
</span>
</Tooltip>
)
}
: {}),
title: typeof x.title === 'string' ? $t(x.title) : x.title
})),
[state.language, serviceType]
)
const translatedPolicyColumns = useMemo(()=>ApprovalPolicyColumns.map((x)=>{
return {
const translatedPublishColumns = useMemo(
() =>
SYSTEM_PUBLISH_ONLINE_COLUMNS.map((x) => {
if (x.dataIndex === 'status') {
return {
...x,
title: $t(x.title),
render: (_: unknown, entity: SystemInsidePublishOnlineItems) => {
switch (entity.status) {
case 'done':
return (
<span className={STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]}>{$t('成功')}</span>
)
case 'error':
return (
<Tooltip title={entity.error || $t('上线失败')}>
<span className={`${STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]} truncate block`}>
{$t('失败')} {entity.error}
</span>
</Tooltip>
)
default:
return <LoadingOutlined className="text-theme" spin />
}
}
}
}
}),
[state.language]
)
const translatedPolicyColumns = useMemo(
() =>
ApprovalPolicyColumns.map((x) => {
return {
...x,
title: typeof x.title === 'string' ? $t(x.title) : x.title,
...(x.dataIndex === 'status' ? {
render:(_,entity)=> (
<span className={`${ApprovalStatusColorClass[entity.change as keyof typeof ApprovalStatusColorClass]} truncate block`}>
{$t(ChangeTypeEnum[entity.change as (keyof typeof ChangeTypeEnum)] || '-')}
...(x.dataIndex === 'status'
? {
render: (_, entity) => (
<span
className={`${ApprovalStatusColorClass[entity.change as keyof typeof ApprovalStatusColorClass]} truncate block`}
>
{$t(ChangeTypeEnum[entity.change as keyof typeof ChangeTypeEnum] || '-')}
</span>
)
}:{})
}
}),[state.language])
)
}
: {})
}
}),
[state.language]
)
return (
<>
{!insidePage && <>
<>
{!insidePage && (
<>
<Row className="my-mbase">
<Col className="text-left" span={4}><span >{$t('申请系统')}</span></Col>
<Col span={18}>{(data as PublishApprovalInfoType).project || '-'}</Col>
<Col className="text-left" span={4}>
<span>{$t('申请系统')}</span>
</Col>
<Col span={18}>{(data as PublishApprovalInfoType).project || '-'}</Col>
</Row>
<Row className="my-mbase">
<Col className="text-left" span={4}><span >{$t('所属团队')}</span></Col>
<Col span={18}>{(data as PublishApprovalInfoType).team || '-'}</Col>
<Col className="text-left" span={4}>
<span>{$t('所属团队')}</span>
</Col>
<Col span={18}>{(data as PublishApprovalInfoType).team || '-'}</Col>
</Row>
<Row className="my-mbase">
<Col className="text-left" span={4}><span >{$t('申请人')}</span></Col>
<Col span={18}>{(data as PublishApprovalInfoType).applier || '-'}</Col>
<Col className="text-left" span={4}>
<span>{$t('申请人')}</span>
</Col>
<Col span={18}>{(data as PublishApprovalInfoType).applier || '-'}</Col>
</Row>
<Row className="my-mbase">
<Col className="text-left" span={4}><span >{$t('申请时间')}</span></Col>
<Col span={18}>{(data as PublishApprovalInfoType).applyTime || '-'}</Col>
<Col className="text-left" span={4}>
<span>{$t('申请时间')}</span>
</Col>
<Col span={18}>{(data as PublishApprovalInfoType).applyTime || '-'}</Col>
</Row>
</> }
<WithPermission access=""><Form
className=" mx-auto"
form={form}
labelAlign='left'
layout='vertical'
scrollToFirstError
name="publishApprovalModalContent"
// labelCol={{span: 3}}
// wrapperCol={{span: 21}}
autoComplete="off"
disabled={type === 'view'}
>
</>
)}
<WithPermission access="">
<Form
className=" mx-auto"
form={form}
labelAlign="left"
layout="vertical"
scrollToFirstError
name="publishApprovalModalContent"
// labelCol={{span: 3}}
// wrapperCol={{span: 21}}
autoComplete="off"
disabled={type === 'view'}
>
{insidePage && (
<>
<Form.Item label={$t('版本号')} name="version" rules={[{ required: true, whitespace: true }]}>
<Input className="w-INPUT_NORMAL" disabled={type !== 'add'} placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
{
insidePage &&
<>
<Form.Item
label={$t("版本号")}
name="version"
rules={[{required: true,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" disabled={type !== 'add'} placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Form.Item
label={$t("版本说明")}
name="versionRemark"
>
<Input.TextArea className="w-INPUT_NORMAL" disabled={type !== 'add' && type !== 'publish'} placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
</>
}
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('路由列表')}</span></Row>
<Row className="mb-mbase ">
<Table
columns={translatedRouteColumns}
bordered={true}
rowKey="id"
size="small"
dataSource={data.diffs?.routers || []}
pagination={false}
/></Row>
{
serviceType === 'rest' && <>
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('上游列表')}</span></Row>
<Row className="mb-mbase ">
<Table
bordered={true}
columns={translatedUpstreamColumns}
size="small"
rowKey="id"
dataSource={data.diffs?.upstreams || []}
pagination={false}
/></Row>
</>
}
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('策略列表')}</span></Row>
<Row className="mb-mbase ">
<Table
bordered={true}
columns={translatedPolicyColumns}
size="small"
rowKey="id"
dataSource={data.diffs?.strategies || []}
pagination={false}
/></Row>
{/* <Form.Item
<Form.Item label={$t('版本说明')} name="versionRemark">
<Input.TextArea
className="w-INPUT_NORMAL"
disabled={type !== 'add' && type !== 'publish'}
placeholder={$t(PLACEHOLDER.input)}
/>
</Form.Item>
</>
)}
<Row className="mt-mbase pb-[8px] h-[32px] font-bold">
<span>{$t('路由列表')}</span>
</Row>
<Row className="mb-mbase ">
<Table
columns={translatedRouteColumns}
bordered={true}
rowKey="id"
size="small"
dataSource={data.diffs?.routers || []}
pagination={false}
/>
</Row>
{serviceType === 'rest' && (
<>
<Row className="mt-mbase pb-[8px] h-[32px] font-bold">
<span>{$t('上游列表')}</span>
</Row>
<Row className="mb-mbase ">
<Table
bordered={true}
columns={translatedUpstreamColumns}
size="small"
rowKey="id"
dataSource={data.diffs?.upstreams || []}
pagination={false}
/>
</Row>
</>
)}
<Row className="mt-mbase pb-[8px] h-[32px] font-bold">
<span>{$t('策略列表')}</span>
</Row>
<Row className="mb-mbase ">
<Table
bordered={true}
columns={translatedPolicyColumns}
size="small"
rowKey="id"
dataSource={data.diffs?.strategies || []}
pagination={false}
/>
</Row>
{/* <Form.Item
label={$t("备注")}
name="remark"
>
<Input.TextArea className="w-INPUT_NORMAL" disabled={type !== 'add' && type !== 'publish'} placeholder={$t(PLACEHOLDER.input)} />
</Form.Item> */}
{/*
{/*
{type !== 'add' && type !== 'publish' && <Form.Item
label={$t("审核意见"
name="opinion"
@@ -264,20 +409,29 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
},
]);}}/>
</Form.Item>} */}
{['error','done'].indexOf(data.status) !== -1 && data.clusterPublishStatus &&data.clusterPublishStatus.length > 0 && <>
<Row className="text-left h-[32px] mb-8px]" span={3}><span>{$t('上线情况')}</span></Row>
<Row span={24} className="mb-mbase">
<Table
bordered={true}
columns={[...translatedPublishColumns]}
size="small"
rowKey="id"
dataSource={data.clusterPublishStatus || []}
pagination={false}
/>
</Row></>}
</Form>
</WithPermission>
</>)
})
{['error', 'done'].indexOf(data.status) !== -1 &&
data.clusterPublishStatus &&
data.clusterPublishStatus.length > 0 && (
<>
<Row className="text-left h-[32px] mb-8px]" span={3}>
<span>{$t('上线情况')}</span>
</Row>
<Row span={24} className="mb-mbase">
<Table
bordered={true}
columns={[...translatedPublishColumns]}
size="small"
rowKey="id"
dataSource={data.clusterPublishStatus || []}
pagination={false}
/>
</Row>
</>
)}
</Form>
</WithPermission>
</>
)
}
)
@@ -1,64 +1,64 @@
import {FC, useRef, useEffect, Children, cloneElement, isValidElement } from 'react';
import { FC, useRef, useEffect, Children, cloneElement, isValidElement } from 'react'
interface ScrollableSectionProps {
children: React.ReactNode;
children: React.ReactNode
}
const ScrollableSection: FC<ScrollableSectionProps> = ({ children }) => {
const scrollAreaRef = useRef<HTMLDivElement>(null);
const scrollAreaRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleScroll = () => {
if (scrollAreaRef.current) {
const scrollTop = scrollAreaRef.current.scrollTop;
const scrollHeight = scrollAreaRef.current.scrollHeight;
const clientHeight = scrollAreaRef.current.clientHeight;
const scrollTop = scrollAreaRef.current.scrollTop
const scrollHeight = scrollAreaRef.current.scrollHeight
const clientHeight = scrollAreaRef.current.clientHeight
// 如果滚动到顶部,.content-before 应该显示阴影
const showTopShadow = scrollTop > 0;
// 如果滚动到底部,.content-after 应该显示阴影
const showBottomShadow = scrollHeight - scrollTop < clientHeight;
// 这里我们不直接更新状态,而是通过ref来设置样式
if (showTopShadow && !showBottomShadow) {
setElementShadow('.content-before', true);
setElementShadow('.content-after', false);
} else if (!showTopShadow && showBottomShadow) {
setElementShadow('.content-before', false);
setElementShadow('.content-after', true);
} else {
setElementShadow('.content-before', false);
setElementShadow('.content-after', false);
}
// 如果滚动到顶部,.content-before 应该显示阴影
const showTopShadow = scrollTop > 0
// 如果滚动到底部,.content-after 应该显示阴影
const showBottomShadow = scrollHeight - scrollTop < clientHeight
// 这里我们不直接更新状态,而是通过ref来设置样式
if (showTopShadow && !showBottomShadow) {
setElementShadow('.content-before', true)
setElementShadow('.content-after', false)
} else if (!showTopShadow && showBottomShadow) {
setElementShadow('.content-before', false)
setElementShadow('.content-after', true)
} else {
setElementShadow('.content-before', false)
setElementShadow('.content-after', false)
}
}
};
}
scrollAreaRef.current?.addEventListener('scroll', handleScroll);
scrollAreaRef.current?.addEventListener('scroll', handleScroll)
return () => {
scrollAreaRef.current?.removeEventListener('scroll', handleScroll);
};
}, []);
scrollAreaRef.current?.removeEventListener('scroll', handleScroll)
}
}, [])
const setElementShadow = (elementSelector: string, showShadow: boolean) => {
const element = document.querySelector(elementSelector);
const element = document.querySelector(elementSelector)
if (element) {
element.style.boxShadow = showShadow ? ( elementSelector === '.content-before' ? '0 2px 2px #0000000d':'0 -2px 2px -2px var(--border-color)') : 'none';
element.style.boxShadow = showShadow
? elementSelector === '.content-before'
? '0 2px 2px #0000000d'
: '0 -2px 2px -2px var(--border-color)'
: 'none'
}
}
const childrenWithRef = Children.toArray(children).map((child) => {
if (isValidElement(child) && child.props.className && child.props.className.includes('scroll-area')) {
// 将 ref 附加到具有 'scroll-area' 类名的子元素
return cloneElement(child, { ref: scrollAreaRef });
return cloneElement(child, { ref: scrollAreaRef })
}
return child;
});
return child
})
return (
<> {childrenWithRef}
</>
);
};
return <> {childrenWithRef}</>
}
export default ScrollableSection;
export default ScrollableSection
@@ -1,115 +1,129 @@
import {App, Col, Form, Input, Row} from "antd";
import { forwardRef, useEffect, useImperativeHandle} from "react";
import {SubscribeApprovalInfoType} from "@common/const/approval/type.tsx";
import {BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { SubscribeApprovalList } from "@common/const/approval/const";
import { $t } from "@common/locales";
import { App, Col, Form, Input, Row } from 'antd'
import { forwardRef, useEffect, useImperativeHandle } from 'react'
import { SubscribeApprovalInfoType } from '@common/const/approval/type.tsx'
import { BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import { useFetch } from '@common/hooks/http.ts'
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { SubscribeApprovalList } from '@common/const/approval/const'
import { $t } from '@common/locales'
type SubscribeApprovalModalProps = {
type:'approval'|'view'
data?:SubscribeApprovalInfoType
inSystem?:boolean
serviceId:string
teamId:string
type: 'approval' | 'view'
data?: SubscribeApprovalInfoType
inSystem?: boolean
serviceId: string
teamId: string
}
export type SubscribeApprovalModalHandle = {
save:(operate:'pass'|'refuse') =>Promise<boolean|string>
save: (operate: 'pass' | 'refuse') => Promise<boolean | string>
}
type FieldType = {
reason?:string;
opinion?:string;
};
reason?: string
opinion?: string
}
export const SubscribeApprovalModalContent = forwardRef<SubscribeApprovalModalHandle,SubscribeApprovalModalProps>((props, ref) => {
export const SubscribeApprovalModalContent = forwardRef<SubscribeApprovalModalHandle, SubscribeApprovalModalProps>(
(props, ref) => {
const { message } = App.useApp()
const {data, type,inSystem=false, teamId, serviceId} = props
const [form] = Form.useForm();
const {fetchData} = useFetch()
const { data, type, inSystem = false, teamId, serviceId } = props
const [form] = Form.useForm()
const { fetchData } = useFetch()
const save:(operate:'pass'|'refuse')=>Promise<boolean | string> = (operate)=>{
return new Promise((resolve, reject)=>{
if(type === 'view'){
resolve(true)
return
}
form.validateFields().then((value)=>{
if(operate === 'refuse' && form.getFieldValue('opinion') === ''){
form.setFields([{
name:'opinion',errors:[$t(FORM_ERROR_TIPS.refuseOpinion)]
}])
form.scrollToField('opinion')
reject($t(RESPONSE_TIPS.refuseOpinion))
return
const save: (operate: 'pass' | 'refuse') => Promise<boolean | string> = (operate) => {
return new Promise((resolve, reject) => {
if (type === 'view') {
resolve(true)
return
}
form
.validateFields()
.then((value) => {
if (operate === 'refuse' && form.getFieldValue('opinion') === '') {
form.setFields([
{
name: 'opinion',
errors: [$t(FORM_ERROR_TIPS.refuseOpinion)]
}
fetchData<BasicResponse<null>>(`${inSystem?'service/':''}approval/subscribe`,{method: 'POST',eoBody:({opinion:value.opinion,operate}), eoParams:(inSystem ? {apply:data!.id, team:teamId} : {id:data!.id,team:teamId})}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo)=> reject(errorInfo))
})
])
form.scrollToField('opinion')
reject($t(RESPONSE_TIPS.refuseOpinion))
return
}
fetchData<BasicResponse<null>>(`${inSystem ? 'service/' : ''}approval/subscribe`, {
method: 'POST',
eoBody: { opinion: value.opinion, operate },
eoParams: inSystem ? { apply: data!.id, team: teamId } : { id: data!.id, team: teamId }
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo) => reject(errorInfo))
})
}
useImperativeHandle(ref, ()=>({
save
})
)
useImperativeHandle(ref, () => ({
save
}))
useEffect(()=>{
form.setFieldsValue({opinion:'',...data})
},[])
useEffect(() => {
form.setFieldsValue({ opinion: '', ...data })
}, [])
return (
<div className="my-btnybase">{
SubscribeApprovalList?.map((x)=>(
<Row key={x.key} className="leading-[32px] mb-btnbase mx-auto">
<Col className="text-left" span={6}>{$t(x.title)}</Col>
<Col >{(data as {[k:string]:unknown})?.[x.key]?.name || (data as {[k:string]:unknown})?.[x.key] || '-'}</Col>
</Row>
))
}
<div className="my-btnybase">
{SubscribeApprovalList?.map((x) => (
<Row key={x.key} className="leading-[32px] mb-btnbase mx-auto">
<Col className="text-left" span={6}>
{$t(x.title)}
</Col>
<Col>
{(data as { [k: string]: unknown })?.[x.key]?.name || (data as { [k: string]: unknown })?.[x.key] || '-'}
</Col>
</Row>
))}
<WithPermission access="">
<Form
labelAlign='left'
layout='vertical'
form={form}
className="mx-auto "
name="subscribeApprovalModalContent"
// labelCol={{ span: 6}}
// wrapperCol={{ span: 18}}
autoComplete="off"
disabled={type === 'view'}
>
<Form.Item<FieldType>
label={$t("申请原因")}
name="reason"
>
<Input.TextArea className="w-INPUT_NORMAL" disabled={true} placeholder=" " />
</Form.Item>
<Form.Item<FieldType>
label={$t("审核意见")}
name="opinion"
extra={$t(FORM_ERROR_TIPS.refuseOpinion)}
>
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} onChange={()=>{ form.setFields([
{
name: 'opinion',
errors: [], // 设置为空数组来移除错误信息
},
])}} />
</Form.Item>
</Form>
</WithPermission>
</div>
<Form
labelAlign="left"
layout="vertical"
form={form}
className="mx-auto "
name="subscribeApprovalModalContent"
// labelCol={{ span: 6}}
// wrapperCol={{ span: 18}}
autoComplete="off"
disabled={type === 'view'}
>
<Form.Item<FieldType> label={$t('申请原因')} name="reason">
<Input.TextArea className="w-INPUT_NORMAL" disabled={true} placeholder=" " />
</Form.Item>
<Form.Item<FieldType> label={$t('审核意见')} name="opinion" extra={$t(FORM_ERROR_TIPS.refuseOpinion)}>
<Input.TextArea
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
onChange={() => {
form.setFields([
{
name: 'opinion',
errors: [] // 设置为空数组来移除错误信息
}
])
}}
/>
</Form.Item>
</Form>
</WithPermission>
</div>
)
})
}
)
@@ -1,41 +1,51 @@
import { Button, Tooltip } from "antd"
import { useState, useMemo, useEffect, useCallback } from "react"
import { useGlobalContext } from "@common/contexts/GlobalStateContext"
import { useNavigate } from "react-router-dom"
import { PERMISSION_DEFINITION } from "@common/const/permissions"
import { Icon } from "@iconify/react/dist/iconify.js"
import { $t } from "@common/locales"
import { PERMISSION_DEFINITION } from '@common/const/permissions'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { $t } from '@common/locales'
import { Icon } from '@iconify/react/dist/iconify.js'
import { Button, Tooltip } from 'antd'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
type TableBtnWithPermissionProps = {
btnTitle: string
access?: keyof typeof PERMISSION_DEFINITION[0],
tooltip?: string,
disabled?: boolean,
navigateTo?: string,
access?: keyof (typeof PERMISSION_DEFINITION)[0]
tooltip?: string
disabled?: boolean
navigateTo?: string
onClick?: (args?: unknown) => void
className?: string
btnType: string
}
const TableIconName = {
'add': 'ic:baseline-add',
'edit': 'ic:baseline-edit',
'delete': 'ic:baseline-delete',
'remove': 'ic:baseline-minus',
'copy': 'ic:baseline-file-copy',
'view': 'ic:baseline-remove-red-eye',
'publish': 'ic:baseline-publish',
'approval': 'ic:baseline-approval',
'stop': 'ic:baseline-stop-circle',
'online': 'ic:baseline-check-circle',
'cancel': 'ic:baseline-cancel-schedule-send',
'refresh': 'ic:baseline-refresh',
'logs': 'hugeicons:google-doc'
add: 'ic:baseline-add',
edit: 'ic:baseline-edit',
delete: 'ic:baseline-delete',
remove: 'ic:baseline-minus',
copy: 'ic:baseline-file-copy',
view: 'ic:baseline-remove-red-eye',
publish: 'ic:baseline-publish',
offline: 'ic:baseline-file-download-off',
approval: 'ic:baseline-approval',
stop: 'ic:baseline-stop-circle',
online: 'ic:baseline-check-circle',
cancel: 'ic:baseline-cancel-schedule-send',
refresh: 'ic:baseline-refresh',
logs: 'hugeicons:google-doc',
disable: 'ic:baseline-pause-circle',
enable: 'ic:baseline-play-circle'
}
// 表格操作栏按钮,受权限控制
const TableBtnWithPermission = ({ btnTitle, access, tooltip, disabled, navigateTo, onClick, className, btnType }: TableBtnWithPermissionProps) => {
const TableBtnWithPermission = ({
btnTitle,
access,
tooltip,
disabled,
navigateTo,
onClick,
className,
btnType
}: TableBtnWithPermissionProps) => {
const [btnAccess, setBtnAccess] = useState<boolean>(false)
const [btnStatus, setBtnStatus] = useState<boolean>(false)
const [closeToolTip, setCloseToolTip] = useState<boolean>(false)
@@ -51,16 +61,18 @@ const TableBtnWithPermission = ({ btnTitle, access, tooltip, disabled, navigateT
access ? setBtnAccess(lastAccess) : setBtnAccess(true)
}, [access, lastAccess])
const handleClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
setTimeout(() => {
setBtnStatus(false)
setCloseToolTip(true)
})
const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
setTimeout(() => {
setBtnStatus(false)
setCloseToolTip(true)
})
navigateTo ? navigate(navigateTo) : onClick?.()
}, [navigateTo, navigate, onClick])
navigateTo ? navigate(navigateTo) : onClick?.()
},
[navigateTo, navigate, onClick]
)
const changeTooltipStatus = (open: boolean) => {
setBtnStatus(open)
if (closeToolTip) {
@@ -68,18 +80,42 @@ const TableBtnWithPermission = ({ btnTitle, access, tooltip, disabled, navigateT
setCloseToolTip(false)
}
}
return (<>{
!btnAccess || (disabled && tooltip) ?
<Tooltip placement="top" title={tooltip ?? $t('暂无(0)权限,请联系管理员分配。', [$t(btnTitle).toLowerCase()])}>
<Button type="text" disabled={true} className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className}`} key={btnType} icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />} >{ }</Button>
</Tooltip>
:
<Tooltip placement="top" title={$t(btnTitle)} trigger='hover' open={btnStatus} onOpenChange={changeTooltipStatus}>
<Button type="text" disabled={disabled} className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className} `} key={btnType} icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />} onClick={handleClick}>{ }</Button>
</Tooltip>
}</>
);
return (
<>
{!btnAccess || (disabled && tooltip) ? (
<Tooltip placement="top" title={tooltip ?? $t('暂无(0)权限,请联系管理员分配。', [$t(btnTitle).toLowerCase()])}>
<Button
type="text"
disabled={true}
className={`flex items-center p-0 bg-transparent border-none h-[22px] ${className}`}
key={btnType}
icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />}
>
{}
</Button>
</Tooltip>
) : (
<Tooltip
placement="top"
title={$t(btnTitle)}
trigger="hover"
open={btnStatus}
onOpenChange={changeTooltipStatus}
>
<Button
type="text"
disabled={disabled}
className={`flex items-center p-0 bg-transparent border-none h-[22px] ${className}`}
key={btnType}
icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />}
onClick={handleClick}
>
{}
</Button>
</Tooltip>
)}
</>
)
}
export default TableBtnWithPermission
export default TableBtnWithPermission
@@ -1,37 +1,38 @@
import { Tag, TagProps } from 'antd'
import { useState, useMemo, useEffect } from 'react'
import { PERMISSION_DEFINITION } from '@common/const/permissions'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { Tag, TagProps } from "antd";
import { useState, useMemo, useEffect } from "react";
import { PERMISSION_DEFINITION } from "@common/const/permissions";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
export interface TagWithPermission extends TagProps{
access?:string
export interface TagWithPermission extends TagProps {
access?: string
}
export default function TagWithPermission(props:TagWithPermission){
const {access,onClose} = props
const [editAccess, setEditAccess] = useState<boolean>(access ? false:true)
const {accessData,checkPermission,accessInit} = useGlobalContext()
const lastAccess = useMemo(()=>{
if(!access) return true
return checkPermission(access as keyof typeof PERMISSION_DEFINITION[0])
},[access, accessData,checkPermission,accessInit])
export default function TagWithPermission(props: TagWithPermission) {
const { access, onClose } = props
const [editAccess, setEditAccess] = useState<boolean>(access ? false : true)
const { accessData, checkPermission, accessInit } = useGlobalContext()
const lastAccess = useMemo(() => {
if (!access) return true
return checkPermission(access as keyof (typeof PERMISSION_DEFINITION)[0])
}, [access, accessData, checkPermission, accessInit])
useEffect(()=>{
access ? setEditAccess(lastAccess) : setEditAccess(true)
},[lastAccess])
const handleTagClose = (e: React.MouseEvent<HTMLElement>)=>{
e.preventDefault();
if(!editAccess) return
onClose?.(e)
}
useEffect(() => {
access ? setEditAccess(lastAccess) : setEditAccess(true)
}, [lastAccess])
return <Tag
closeIcon
{...props}
className={` rounded-SEARCH_RADIUS h-[32px] text-[14px] leading-[22px] py-[5px] px-btnbase bg-transparent mb-[8px] ${props.className}`}
onClose={handleTagClose}>
{props.children}
</Tag>
const handleTagClose = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault()
if (!editAccess) return
onClose?.(e)
}
}
return (
<Tag
closeIcon
{...props}
className={` rounded-SEARCH_RADIUS h-[32px] text-[14px] leading-[22px] py-[5px] px-btnbase bg-transparent mb-[8px] ${props.className}`}
onClose={handleTagClose}
>
{props.children}
</Tag>
)
}
@@ -1,34 +1,33 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react'
const ThemeSwitcher = () => {
const [darkMode, setDarkMode] = useState(true);
const [darkMode, setDarkMode] = useState(true)
useEffect(() => {
let isDarkMode = localStorage.getItem('dark-mode');
if(isDarkMode !== undefined && isDarkMode !== null){
setDarkMode(isDarkMode === 'true')
}else{
localStorage.setItem('dark-mode', (darkMode).toString());
const isDarkMode = localStorage.getItem('dark-mode')
if (isDarkMode !== undefined && isDarkMode !== null) {
setDarkMode(isDarkMode === 'true')
} else {
localStorage.setItem('dark-mode', darkMode.toString())
}
}, []);
}, [])
useEffect(()=>{
document.documentElement.classList.toggle('dark', darkMode);
},[darkMode])
useEffect(() => {
document.documentElement.classList.toggle('dark', darkMode)
}, [darkMode])
const toggleDarkMode = () => {
setDarkMode(!darkMode);
localStorage.setItem('dark-mode', (!darkMode).toString());
document.documentElement.classList.toggle('dark', !darkMode);
};
setDarkMode(!darkMode)
localStorage.setItem('dark-mode', (!darkMode).toString())
document.documentElement.classList.toggle('dark', !darkMode)
}
return (
// <button onClick={toggleDarkMode}>
// {darkMode ? '切换到白天模式' : '切换到黑夜模式'}
// </button>
<></>
);
};
)
}
export default ThemeSwitcher;
export default ThemeSwitcher
@@ -1,25 +1,24 @@
import { useEffect, useState } from 'react'
import { Radio, DatePicker, GetProps, RadioChangeEvent } from 'antd'
import dayjs, { Dayjs } from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import '../../index.css'
import { $t } from '@common/locales'
import { useEffect, useState } from 'react';
import { Radio, DatePicker, GetProps, RadioChangeEvent } from 'antd';
import dayjs, { Dayjs } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import "../../index.css"
import { $t } from '@common/locales';
type RangePickerProps = GetProps<typeof DatePicker.RangePicker>
export type RangeValue = [Dayjs | null, Dayjs | null] | null
type RangePickerProps = GetProps<typeof DatePicker.RangePicker>;
export type RangeValue = [Dayjs | null, Dayjs | null] | null;
dayjs.extend(customParseFormat);
dayjs.extend(customParseFormat)
export type TimeRange = {
start: number | null
end: number | null
}
export type TimeRangeButton = '' | 'hour' | 'day' | 'threeDays' | 'sevenDays';
export type TimeRangeButton = '' | 'hour' | 'day' | 'threeDays' | 'sevenDays'
type TimeRangeSelectorProps = {
initialTimeButton?: TimeRangeButton,
initialTimeButton?: TimeRangeButton
initialDatePickerValue?: RangeValue
onTimeRangeChange?: (timeRange: TimeRange) => void
hideTitle?: boolean
@@ -30,17 +29,27 @@ type TimeRangeSelectorProps = {
defaultTimeButton?: TimeRangeButton
}
const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
const { initialTimeButton, initialDatePickerValue, onTimeRangeChange, hideTitle, onTimeButtonChange, labelSize = 'default', bindRef, hideBtns = [], defaultTimeButton = 'hour' } = props
const [timeButton, setTimeButton] = useState(initialTimeButton || '');
const [datePickerValue, setDatePickerValue] = useState<RangeValue>(initialDatePickerValue || [null, null]);
const {
initialTimeButton,
initialDatePickerValue,
onTimeRangeChange,
hideTitle,
onTimeButtonChange,
labelSize = 'default',
bindRef,
hideBtns = [],
defaultTimeButton = 'hour'
} = props
const [timeButton, setTimeButton] = useState(initialTimeButton || '')
const [datePickerValue, setDatePickerValue] = useState<RangeValue>(initialDatePickerValue || [null, null])
useEffect(() => {
if (bindRef) {
bindRef({ reset });
bindRef({ reset })
}
}, [bindRef])
// 根据选择的时间范围计算开始和结束时间
const calculateTimeRange = (curBtn: TimeRangeButton) => {
const currentSecond = Math.floor(Date.now() / 1000); // 当前秒级时间戳
const currentSecond = Math.floor(Date.now() / 1000) // 当前秒级时间戳
let startMin = currentSecond - 60 * 60
switch (curBtn) {
case 'hour': {
@@ -52,30 +61,26 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
break
}
case 'threeDays': {
startMin =
Math.floor(new Date().setHours(0, 0, 0, 0) / 1000) -
2 * 24 * 60 * 60
startMin = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000) - 2 * 24 * 60 * 60
break
}
case 'sevenDays': {
startMin =
Math.floor(new Date().setHours(0, 0, 0, 0) / 1000) -
6 * 24 * 60 * 60
startMin = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000) - 6 * 24 * 60 * 60
break
}
}
if (onTimeRangeChange) {
onTimeRangeChange({ start: startMin, end: currentSecond });
onTimeRangeChange({ start: startMin, end: currentSecond })
}
};
}
// 处理单选按钮的变化
const handleRadioChange = (e: RadioChangeEvent) => {
setTimeButton(e.target.value);
setTimeButton(e.target.value)
onTimeButtonChange?.(e.target.value)
setDatePickerValue(null)
calculateTimeRange(e.target.value);
};
calculateTimeRange(e.target.value)
}
const reset = () => {
setTimeButton(defaultTimeButton)
calculateTimeRange(defaultTimeButton)
@@ -86,35 +91,43 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
const handleDatePickerChange = (dates: RangeValue) => {
setTimeButton(dates ? '' : defaultTimeButton)
onTimeButtonChange?.(dates ? '' : defaultTimeButton)
setDatePickerValue(dates);
setDatePickerValue(dates)
if (dates && Array.isArray(dates) && dates.length === 2) {
const [startDate, endDate] = dates;
const start = startDate!.startOf('day').unix(); // 开始日期的00:00:00
const end = endDate!.endOf('day').unix(); // 结束日期的23:59:59
const [startDate, endDate] = dates
const start = startDate!.startOf('day').unix() // 开始日期的00:00:00
const end = endDate!.endOf('day').unix() // 结束日期的23:59:59
if (onTimeRangeChange) {
onTimeRangeChange({ start, end });
onTimeRangeChange({ start, end })
}
}
if (!dates) {
calculateTimeRange(defaultTimeButton)
}
};
}
const disabledDate: RangePickerProps['disabledDate'] = (current) => {
// Can not select days before today and today
return current && current.valueOf() > dayjs().startOf('day').valueOf();
};
return current && current.valueOf() > dayjs().startOf('day').valueOf()
}
return (
<div className="flex flex-nowrap items-center pt-btnybase mr-btnybase">
{!hideTitle && <label className={`whitespace-nowrap `}>{$t('时间')}</label>}
<Radio.Group className="whitespace-nowrap" value={timeButton} onChange={handleRadioChange} buttonStyle="solid">
{hideBtns?.length && hideBtns.includes('hour') ? null : <Radio.Button value="hour">{$t('近1小时')}</Radio.Button>}
{hideBtns?.length && hideBtns.includes('day') ? null : <Radio.Button value="day">{$t('近24小时')}</Radio.Button>}
{hideBtns?.length && hideBtns.includes('threeDays') ? null : <Radio.Button value="threeDays">{$t('近3天')}</Radio.Button>}
{hideBtns?.length && hideBtns.includes('sevenDays') ? null : <Radio.Button className="rounded-e-none" value="sevenDays">{$t('近7天')}</Radio.Button>}
{hideBtns?.length && hideBtns.includes('hour') ? null : (
<Radio.Button value="hour">{$t('近1小时')}</Radio.Button>
)}
{hideBtns?.length && hideBtns.includes('day') ? null : (
<Radio.Button value="day">{$t('近24小时')}</Radio.Button>
)}
{hideBtns?.length && hideBtns.includes('threeDays') ? null : (
<Radio.Button value="threeDays">{$t('近3天')}</Radio.Button>
)}
{hideBtns?.length && hideBtns.includes('sevenDays') ? null : (
<Radio.Button className="rounded-e-none" value="sevenDays">
{$t('近7天')}
</Radio.Button>
)}
</Radio.Group>
<DatePicker.RangePicker
value={datePickerValue}
@@ -129,7 +142,7 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
}}
/>
</div>
);
};
)
}
export default TimeRangeSelector;
export default TimeRangeSelector
@@ -1,45 +1,94 @@
import {CheckOutlined, LoadingOutlined, MoreOutlined} from "@ant-design/icons";
import {Dropdown, Input, InputRef, MenuProps} from "antd";
import { ReactNode, useEffect, useRef, useState} from "react";
import { CheckOutlined, LoadingOutlined, MoreOutlined } from '@ant-design/icons'
import { Dropdown, Input, InputRef, MenuProps } from 'antd'
import { ReactNode, useEffect, useRef, useState } from 'react'
export type TreeWithMoreProp = {
children:ReactNode,
dropdownMenu:MenuProps['items']
editable?:boolean
editingId?:string
afterEdit?:(val:string)=>Promise<string|boolean>
editKey?:string
entity?:{id:string,[k:string]:unknown | string}
onBlur?:()=>void
stopClick?:boolean
children: ReactNode
dropdownMenu: MenuProps['items']
editable?: boolean
editingId?: string
afterEdit?: (val: string) => Promise<string | boolean>
editKey?: string
entity?: { id: string; [k: string]: unknown | string }
onBlur?: () => void
stopClick?: boolean
}
const TreeWithMore = ({children,dropdownMenu,editable,editingId,entity,editKey='name',afterEdit,onBlur,stopClick=true}:TreeWithMoreProp)=>{
const [editValue, setEditValue] = useState<string>(entity?.[editKey] as string)
const [submitting, setSubmitting] = useState<boolean>(false)
const inputRef = useRef<InputRef>(null)
const TreeWithMore = ({
children,
dropdownMenu,
editable,
editingId,
entity,
editKey = 'name',
afterEdit,
onBlur,
stopClick = true
}: TreeWithMoreProp) => {
const [editValue, setEditValue] = useState<string>(entity?.[editKey] as string)
const [submitting, setSubmitting] = useState<boolean>(false)
const inputRef = useRef<InputRef>(null)
const handleSubmit = (val:string)=>{
if(submitting) return
setSubmitting(true)
afterEdit && afterEdit(val).finally(()=>setSubmitting(false))
}
const handleSubmit = (val: string) => {
if (submitting) return
setSubmitting(true)
afterEdit && afterEdit(val).finally(() => setSubmitting(false))
}
useEffect(()=>{inputRef.current?.focus()},[inputRef])
useEffect(() => {
inputRef.current?.focus()
}, [inputRef])
return (<>
{
editable && editingId && entity?.id && editingId === entity.id ? <Input ref={inputRef} value={editValue} onChange={(e)=>{setEditValue(e.target.value)}} onBlur={()=>{onBlur?.()}} onClick={(e)=>stopClick&&e?.stopPropagation()} onPressEnter={()=>{handleSubmit(editValue)}} suffix={submitting ? <LoadingOutlined />:<CheckOutlined onClick={()=>{handleSubmit(editValue)}}/>} />:
<Dropdown menu={{items:dropdownMenu}} trigger={['contextMenu']} >
<div className='tree-title-hover' >{children}
<span onClick={(e)=>{ stopClick && e.stopPropagation();}}>
<Dropdown menu={{items:dropdownMenu}} trigger={['click']} >
<MoreOutlined className="tree-title-more" onClick={(e)=>{ stopClick && e.stopPropagation(); }} />
</Dropdown>
</span>
</div>
return (
<>
{editable && editingId && entity?.id && editingId === entity.id ? (
<Input
ref={inputRef}
value={editValue}
onChange={(e) => {
setEditValue(e.target.value)
}}
onBlur={() => {
onBlur?.()
}}
onClick={(e) => stopClick && e?.stopPropagation()}
onPressEnter={() => {
handleSubmit(editValue)
}}
suffix={
submitting ? (
<LoadingOutlined />
) : (
<CheckOutlined
onClick={() => {
handleSubmit(editValue)
}}
/>
)
}
/>
) : (
<Dropdown menu={{ items: dropdownMenu }} trigger={['contextMenu']}>
<div className="tree-title-hover">
{children}
<span
onClick={(e) => {
stopClick && e.stopPropagation()
}}
>
<Dropdown menu={{ items: dropdownMenu }} trigger={['click']}>
<MoreOutlined
className="tree-title-more"
onClick={(e) => {
stopClick && e.stopPropagation()
}}
/>
</Dropdown>
</span>
</div>
</Dropdown>
}</>)
)}
</>
)
}
export default TreeWithMore
export default TreeWithMore
@@ -1,190 +1,190 @@
import { $t } from "@common/locales"
import { $t } from '@common/locales'
/* 本组件不在页面渲染,只是为了让i18next-scanner能找到从接口传递的、需要翻译的字段 , 此处的字段除非确认不在页面上渲染了,否则不应删除,容易导致翻译遗漏*/
export const TranslateWord = ()=>{
return (
<>
{$t('文件日志')}
{$t('HTTP日志')}
{$t('Kafka日志')}
{$t('NSQ日志')}
{$t('Syslog日志')}
{$t('未分配')}
{$t('超级管理员')}
{$t('团队管理员')}
{$t('运维管理员')}
{$t('普通成员')}
{$t('只读成员')}
{$t('服务管理员')}
{$t('服务开发者')}
{$t('消费者开发者')}
{$t('消费者管理员')}
{$t('驱动名称')}
{$t('请求失败数')}
{$t('转发失败数')}
{$t('作用范围')}
{$t('添加条目')}
{$t('添加地址')}
{$t('文件名称')}
{$t('存放目录')}
{$t('日志分割周期')}
{$t('过期时间')}
{$t('单位:天')}
{$t('输出格式')}
{$t('格式化配置')}
{$t('服务器地址')}
{$t('Access日志')}
{$t('NSQD地址列表')}
{$t('鉴权Secret')}
{$t('网络协议')}
{$t('日志等级')}
{$t('单行')}
{$t('小时')}
{$t('天')}
{$t('未发布')}
{$t('待发布')}
{$t('单位:s,最小值:1')}
{$t('上传文件')}
{$t('替换文件')}
{$t('是否放行')}
{$t('监控')}
{$t('必填')}
{$t('字符非法,仅支持英文')}
{$t('上传 OpenAPI 文档 (.json/.yaml)')}
{$t('替换 OpenAPI 文档 (.json/.yaml)')}
{$t('打开 OpenAPI YAML 编辑器')}
{$t('无需审核:允许任何消费者调用该服务')}
{$t('人工审核:仅允许通过人工审核的消费者调用该服务')}
{$t('永久')}
{$t('否')}
{$t('是')}
{$t('无需审核')}
{$t('需要审核')}
{$t('创建时间')}
{$t('协议')}
{$t('方法')}
{$t('地址(IP 端口或域名)')}
{$t('权重(0-999')}
{$t('带权轮询')}
{$t('发布版本')}
{$t('发布申请记录')}
{$t('创建版本时间')}
{$t('版本状态')}
{$t('创建人')}
{$t('审核时间')}
{$t('申请方-消费者')}
{$t('审核状态')}
{$t('申请人')}
{$t('审核人')}
{$t('审核时间')}
{$t('来源')}
{$t('订阅时间')}
{$t('请输入')}
{$t('请选择')}
{$t('创建者')}
{$t('服务数量')}
{$t('负责人')}
{$t('姓名')}
{$t('团队角色')}
{$t('添加日期')}
{$t('请求成功数')}
{$t('转发成功数')}
{$t('API 名称')}
{$t('失败状态码数')}
{$t('所属服务')}
{$t('平均响应时间(ms)')}
{$t('最大响应时间(ms)')}
{$t('最小响应时间(ms)')}
{$t('平均请求流量(KB)')}
{$t('最大请求流量(KB)')}
{$t('最小请求流量(KB)')}
{$t('所有成员')}
{$t('状态')}
{$t('角色名称')}
{$t('绑定域名')}
{$t('过期日期')}
{$t('支持字母开头、英文数字中横线下划线组合')}
{$t('英文数字下划线任意一种,首字母必须为英文')}
{$t('字符非法,仅支持英文')}
{$t('无法连接集群,请检查集群地址是否正确或防火墙配置')}
{$t('选择拒绝时,审核意见为必填')}
{$t('操作成功')}
{$t('操作失败')}
{$t('正在操作')}
{$t('正在加载数据')}
{$t('获取数据失败')}
{$t('登录成功')}
{$t('退出成功,将跳转至登录页')}
{$t('未填写审核意见')}
{$t('复制成功')}
{$t('复制失败,请手动复制')}
{$t('服务所属团队')}
{$t('在线')}
{$t('已拒绝')}
{$t('中止')}
{$t('发布异常')}
{$t('发布中')}
{$t('申请方所属团队')}
{$t('发布状态')}
{$t(' 次')}
{$t('每分钟')}
{$t('每5分钟')}
{$t('每小时')}
{$t('每天')}
{$t('每周')}
{$t('上线结果')}
{$t('订阅服务数量')}
{$t('鉴权数量')}
{$t('列表')}
{$t('块')}
{$t('HTTP 请求头')}
{$t('全等匹配')}
{$t('前缀匹配')}
{$t('后缀匹配')}
{$t('子串匹配')}
{$t('非等匹配')}
{$t('空值匹配')}
{$t('存在匹配')}
{$t('不存在匹配')}
{$t('区分大小写的正则匹配')}
{$t('不区分大小写的正则匹配')}
{$t('任意匹配')}
{$t('驳回')}
{$t('已订阅')}
{$t('取消申请')}
{$t('透传客户端请求 Host')}
{$t('使用上游服务 Host')}
{$t('重写 Host')}
{$t('动态服务发现')}
{$t('地址')}
{$t('新增')}
{$t('申请方消费者')}
{$t('策略名称')}
{$t('优先级')}
{$t('筛选条件')}
{$t('处理数')}
{$t('数据格式')}
{$t('关键字')}
{$t('正则表达式')}
{$t('手机号')}
{$t('身份证号')}
{$t('银行卡号')}
{$t('金额')}
{$t('日期')}
{$t('局部显示')}
{$t('局部遮蔽')}
{$t('截取')}
{$t('替换')}
{$t('乱序')}
{$t('随机字符串')}
{$t('自定义字符串')}
{$t('请输入IP地址或CIDR范围,每条以换行分割')}
{$t('待更新')}
{$t('待删除')}
{$t('内容')}
{$t('调用地址')}
{$t('消费者 IP')}
{$t('鉴权名称')}
</>
)
}
export const TranslateWord = () => {
return (
<>
{$t('文件日志')}
{$t('HTTP日志')}
{$t('Kafka日志')}
{$t('NSQ日志')}
{$t('Syslog日志')}
{$t('未分配')}
{$t('超级管理员')}
{$t('团队管理员')}
{$t('运维管理员')}
{$t('普通成员')}
{$t('只读成员')}
{$t('服务管理员')}
{$t('服务开发者')}
{$t('消费者开发者')}
{$t('消费者管理员')}
{$t('驱动名称')}
{$t('请求失败数')}
{$t('转发失败数')}
{$t('作用范围')}
{$t('添加条目')}
{$t('添加地址')}
{$t('文件名称')}
{$t('存放目录')}
{$t('日志分割周期')}
{$t('过期时间')}
{$t('单位:天')}
{$t('输出格式')}
{$t('格式化配置')}
{$t('服务器地址')}
{$t('Access日志')}
{$t('NSQD地址列表')}
{$t('鉴权Secret')}
{$t('网络协议')}
{$t('日志等级')}
{$t('单行')}
{$t('小时')}
{$t('天')}
{$t('未发布')}
{$t('待发布')}
{$t('单位:s,最小值:1')}
{$t('上传文件')}
{$t('替换文件')}
{$t('是否放行')}
{$t('监控')}
{$t('必填')}
{$t('字符非法,仅支持英文')}
{$t('上传 OpenAPI 文档 (.json/.yaml)')}
{$t('替换 OpenAPI 文档 (.json/.yaml)')}
{$t('打开 OpenAPI YAML 编辑器')}
{$t('无需审核:允许任何消费者调用该服务')}
{$t('人工审核:仅允许通过人工审核的消费者调用该服务')}
{$t('永久')}
{$t('否')}
{$t('是')}
{$t('无需审核')}
{$t('需要审核')}
{$t('创建时间')}
{$t('协议')}
{$t('方法')}
{$t('地址(IP 端口或域名)')}
{$t('权重(0-999')}
{$t('带权轮询')}
{$t('发布版本')}
{$t('发布申请记录')}
{$t('创建版本时间')}
{$t('版本状态')}
{$t('创建人')}
{$t('审核时间')}
{$t('申请方-消费者')}
{$t('审核状态')}
{$t('申请人')}
{$t('审核人')}
{$t('审核时间')}
{$t('来源')}
{$t('订阅时间')}
{$t('请输入')}
{$t('请选择')}
{$t('创建者')}
{$t('服务数量')}
{$t('负责人')}
{$t('姓名')}
{$t('团队角色')}
{$t('添加日期')}
{$t('请求成功数')}
{$t('转发成功数')}
{$t('API 名称')}
{$t('失败状态码数')}
{$t('所属服务')}
{$t('平均响应时间(ms)')}
{$t('最大响应时间(ms)')}
{$t('最小响应时间(ms)')}
{$t('平均请求流量(KB)')}
{$t('最大请求流量(KB)')}
{$t('最小请求流量(KB)')}
{$t('所有成员')}
{$t('状态')}
{$t('角色名称')}
{$t('绑定域名')}
{$t('过期日期')}
{$t('支持字母开头、英文数字中横线下划线组合')}
{$t('英文数字下划线任意一种,首字母必须为英文')}
{$t('字符非法,仅支持英文')}
{$t('无法连接集群,请检查集群地址是否正确或防火墙配置')}
{$t('选择拒绝时,审核意见为必填')}
{$t('操作成功')}
{$t('操作失败')}
{$t('正在操作')}
{$t('正在加载数据')}
{$t('获取数据失败')}
{$t('登录成功')}
{$t('退出成功,将跳转至登录页')}
{$t('未填写审核意见')}
{$t('复制成功')}
{$t('复制失败,请手动复制')}
{$t('服务所属团队')}
{$t('在线')}
{$t('已拒绝')}
{$t('中止')}
{$t('发布异常')}
{$t('发布中')}
{$t('申请方所属团队')}
{$t('发布状态')}
{$t(' 次')}
{$t('每分钟')}
{$t('每5分钟')}
{$t('每小时')}
{$t('每天')}
{$t('每周')}
{$t('上线结果')}
{$t('订阅服务数量')}
{$t('鉴权数量')}
{$t('列表')}
{$t('块')}
{$t('HTTP 请求头')}
{$t('全等匹配')}
{$t('前缀匹配')}
{$t('后缀匹配')}
{$t('子串匹配')}
{$t('非等匹配')}
{$t('空值匹配')}
{$t('存在匹配')}
{$t('不存在匹配')}
{$t('区分大小写的正则匹配')}
{$t('不区分大小写的正则匹配')}
{$t('任意匹配')}
{$t('驳回')}
{$t('已订阅')}
{$t('取消申请')}
{$t('透传客户端请求 Host')}
{$t('使用上游服务 Host')}
{$t('重写 Host')}
{$t('动态服务发现')}
{$t('地址')}
{$t('新增')}
{$t('申请方消费者')}
{$t('策略名称')}
{$t('优先级')}
{$t('筛选条件')}
{$t('处理数')}
{$t('数据格式')}
{$t('关键字')}
{$t('正则表达式')}
{$t('手机号')}
{$t('身份证号')}
{$t('银行卡号')}
{$t('金额')}
{$t('日期')}
{$t('局部显示')}
{$t('局部遮蔽')}
{$t('截取')}
{$t('替换')}
{$t('乱序')}
{$t('随机字符串')}
{$t('自定义字符串')}
{$t('请输入IP地址或CIDR范围,每条以换行分割')}
{$t('待更新')}
{$t('待删除')}
{$t('内容')}
{$t('调用地址')}
{$t('消费者 IP')}
{$t('鉴权名称')}
</>
)
}
@@ -1,49 +1,47 @@
import { Button, Tooltip, Upload } from "antd";
import { ReactElement, cloneElement, useEffect, useMemo, useState } from "react";
import { useGlobalContext } from "../../contexts/GlobalStateContext";
import { PERMISSION_DEFINITION } from "@common/const/permissions";
import { $t } from "@common/locales";
import { last } from "lodash-es";
import { Button, Tooltip, Upload } from 'antd'
import { ReactElement, cloneElement, useEffect, useMemo, useState } from 'react'
import { useGlobalContext } from '../../contexts/GlobalStateContext'
import { PERMISSION_DEFINITION } from '@common/const/permissions'
import { $t } from '@common/locales'
type WithPermissionProps = {
access?:string | string[]
tooltip?:string
children:ReactElement
disabled?:boolean
showDisabled?:boolean
access?: string | string[]
tooltip?: string
children: ReactElement
disabled?: boolean
showDisabled?: boolean
}
// 权限控制的高阶组件
const WithPermission = ({access, tooltip, children,disabled, showDisabled = true}:WithPermissionProps) => {
const [editAccess, setEditAccess] = useState<boolean>(access ? false:true)
const {accessData,checkPermission,accessInit} = useGlobalContext()
const WithPermission = ({ access, tooltip, children, disabled, showDisabled = true }: WithPermissionProps) => {
const [editAccess, setEditAccess] = useState<boolean>(access ? false : true)
const { accessData, checkPermission, accessInit } = useGlobalContext()
const lastAccess = useMemo(()=>{
if(!access) return true
return checkPermission(access as keyof typeof PERMISSION_DEFINITION[0])
},[access, accessData,checkPermission,accessInit])
const lastAccess = useMemo(() => {
if (!access) return true
return checkPermission(access as keyof (typeof PERMISSION_DEFINITION)[0])
}, [access, accessData, checkPermission, accessInit])
useEffect(()=>{
// 先判断权限,无论权限是否为true,如果disabled为true时则必须为ture
access && setEditAccess(lastAccess)
},[lastAccess,disabled])
useEffect(() => {
// 先判断权限,无论权限是否为true,如果disabled为true时则必须为ture
access && setEditAccess(lastAccess)
}, [lastAccess, disabled])
return (
<>
{}
{editAccess && !disabled && cloneElement(children)}
{editAccess && disabled && <Tooltip title={tooltip}>{cloneElement(children, { disabled: true })}</Tooltip>}
{!editAccess && children?.type !== Button && children?.type !== Upload && showDisabled && (
<Tooltip title={tooltip ?? $t('暂无操作权限,请联系管理员分配。')}>
{cloneElement(children, {
disabled: true,
onClick: (e) => e.preventDefault(),
okButtonProps: { disabled: true }
})}
</Tooltip>
)}
</>
)
}
return (
<>{
}
{editAccess && !disabled && cloneElement(children)}
{editAccess && disabled && <Tooltip title={tooltip}>
{ cloneElement(children, {disabled:true})}
</Tooltip>}
{!editAccess && (children?.type !== Button && children?.type !== Upload && showDisabled) && <Tooltip title={tooltip ?? $t("暂无操作权限,请联系管理员分配。")}>
{ cloneElement(children, {disabled:true, onClick:(e)=>e.preventDefault(),okButtonProps:{disabled:true}})}
</Tooltip>}
</>
);
}
export default WithPermission
export default WithPermission
@@ -1,79 +1,85 @@
import { set } from 'lodash-es';
import { ExoticComponent, JSXElementConstructor, ReactElement, useEffect, useState } from 'react';
import { useBlocker, useLocation, useNavigate } from 'react-router-dom';
import { JSX } from 'react/jsx-runtime';
import { ExoticComponent, JSXElementConstructor, useEffect, useState } from 'react'
import { useBlocker, useLocation } from 'react-router-dom'
import { JSX } from 'react/jsx-runtime'
const withRouteGuard = (WrappedComponent: ExoticComponent<any> | JSXElementConstructor<any>, {
canActivate,
canLoad ,
canDeactivate,
deactivated,
pathPrefix
}: { pathPrefix?:string, canActivate?: () => Promise<boolean>; canLoad?: () => Promise<boolean>; canDeactivate?: () => Promise<boolean>; deactivated?: () => Promise<void>; } = {}) => {
const withRouteGuard = (
WrappedComponent: ExoticComponent<any> | JSXElementConstructor<any>,
{
canActivate,
canLoad,
canDeactivate,
deactivated,
pathPrefix
}: {
pathPrefix?: string
canActivate?: () => Promise<boolean>
canLoad?: () => Promise<boolean>
canDeactivate?: () => Promise<boolean>
deactivated?: () => Promise<void>
} = {}
) => {
return function RouteGuard(props: JSX.IntrinsicAttributes) {
const [isActivated, setIsActivated] = useState<boolean>(false);
const location = useLocation();
const [isActivated, setIsActivated] = useState<boolean>(false)
const location = useLocation()
// check canActivate
const startLifecycle = async ()=>{
if(canActivate){
const activateRes = await canActivate();
setIsActivated(activateRes);
}else{
setIsActivated(true);
const startLifecycle = async () => {
if (canActivate) {
const activateRes = await canActivate()
setIsActivated(activateRes)
} else {
setIsActivated(true)
}
}
// check canDeactivate
const handleBeforeUnload = async (event: { preventDefault: () => void; returnValue: string }) => {
const deactivateRes = canDeactivate ? await canDeactivate() : true
if (!deactivateRes) {
event.preventDefault()
event.returnValue = ''
}
}
// check canDeactivate
const handleBeforeUnload =async (event: { preventDefault: () => void; returnValue: string; }) => {
const deactivateRes = canDeactivate? await canDeactivate():true;
if (!deactivateRes) {
event.preventDefault();
event.returnValue = '';
}
};
// 激活组件时的检查
useEffect(() => {
startLifecycle();
window.addEventListener('beforeunload', handleBeforeUnload);
startLifecycle()
window.addEventListener('beforeunload', handleBeforeUnload)
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
deactivated?.();
};
}, []);
window.removeEventListener('beforeunload', handleBeforeUnload)
deactivated?.()
}
}, [])
const blocker = useBlocker((tx) => {
const currentPath = location.pathname;
const targetPath = tx.nextLocation.pathname;
const currentPath = location.pathname
const targetPath = tx.nextLocation.pathname
if (pathPrefix && currentPath.startsWith(pathPrefix) && !targetPath.startsWith(pathPrefix) && canDeactivate) {
if (pathPrefix && currentPath.startsWith(pathPrefix) && !targetPath.startsWith(pathPrefix) && canDeactivate) {
canDeactivate().then((res) => {
if(res){
return false;
}else{
return true;
if (res) {
return false
} else {
return true
}
})
} else {
return false
}
});
})
const checkCanLoad = async()=>{
const loadRes = await canLoad!();
!loadRes && setIsActivated(false);
const checkCanLoad = async () => {
const loadRes = await canLoad!()
!loadRes && setIsActivated(false)
}
useEffect(() => {
if (isActivated && canLoad) {
checkCanLoad()
}
}, [isActivated]);
}, [isActivated])
return isActivated ? <WrappedComponent {...props}/> : null;
};
return isActivated ? <WrappedComponent {...props} /> : null
}
}
export default withRouteGuard;
export default withRouteGuard
@@ -1,150 +1,143 @@
import {forwardRef, useImperativeHandle, useState} from 'react'
import { forwardRef, useImperativeHandle, useState } from 'react'
import { Input } from 'antd'
import { Icon } from '@iconify/react/dist/iconify.js'
export const ArrayItemBlankComponent = forwardRef(
(props: { [k: string]: any }, ref) => {
const { onChange, value, dataFormat } = props
export const ArrayItemBlankComponent = forwardRef((props: { [k: string]: any }, ref) => {
const { onChange, value, dataFormat } = props
const getDefaultListItem = () => {
const defaultData: { [k: string]: unknown } = {}
const getDefaultListItem = () => {
const defaultData: { [k: string]: unknown } = {}
for (const data of dataFormat) {
defaultData[data.key] = ''
}
return [defaultData]
for (const data of dataFormat) {
defaultData[data.key] = ''
}
const [resList, setResList] = useState(
value && Object.keys(value).length > 0
? [
...value
?.filter((v: string) => {
return v
})
?.map((v: string) => {
const vTmp = v
const newValue: { [k: string]: unknown } = {}
for (let index = 0; index < dataFormat.length; index++) {
if (dataFormat[index]?.hideName) {
newValue[dataFormat[index].key] = vTmp.split(' ')[index]
} else {
const vTmp2: string | string[] | undefined =
vTmp.indexOf(' ') === -1
? vTmp
: vTmp.split(' ')[index]
return [defaultData]
}
const [resList, setResList] = useState(
value && Object.keys(value).length > 0
? [
...value
?.filter((v: string) => {
return v
})
?.map((v: string) => {
const vTmp = v
const newValue: { [k: string]: unknown } = {}
for (let index = 0; index < dataFormat.length; index++) {
if (dataFormat[index]?.hideName) {
newValue[dataFormat[index].key] = vTmp.split(' ')[index]
} else {
const vTmp2: string | string[] | undefined =
vTmp.indexOf(' ') === -1
? vTmp
: vTmp.split(' ')[index]
? vTmp.split(' ')[index].indexOf('=') === -1
? ''
: vTmp.split(' ')[index].split('=')
: ''
if (vTmp2 && vTmp2 instanceof Array && vTmp2.length > 0) {
vTmp2.shift()
}
newValue[dataFormat[index].key] =
vTmp2 instanceof Array ? vTmp2?.join('=') : vTmp2
if (vTmp2 && vTmp2 instanceof Array && vTmp2.length > 0) {
vTmp2.shift()
}
newValue[dataFormat[index].key] = vTmp2 instanceof Array ? vTmp2?.join('=') : vTmp2
}
return newValue
}),
...getDefaultListItem()
]
: [...getDefaultListItem()]
)
}
return newValue
}),
...getDefaultListItem()
]
: [...getDefaultListItem()]
)
useImperativeHandle(ref, () => ({}))
useImperativeHandle(ref, () => ({}))
const emitNewArr = () => {
const newArr: Array<string> = []
for (const r of resList) {
if (r[dataFormat[0].key]) {
newArr.push(
dataFormat?.map((format: { key: string; hideName: boolean }) => {
return format?.hideName
? r[format.key]
: `${format.key}=${r[format.key]}`
})
.join(' ')
)
}
}
onChange(newArr)
}
const changeInputValue = (
newValue: string,
index: number,
keyName: string,
dataFormat: unknown
) => {
const newArr = [...resList]
newArr[index][keyName] = newValue
newArr[index].status =
(dataFormat.required && !newValue) ||
(dataFormat.pattern && !dataFormat.pattern.test(newValue))
? 'error'
: ''
setResList(newArr)
emitNewArr()
if (index === resList.length - 1) {
setResList([...newArr, ...getDefaultListItem()])
const emitNewArr = () => {
const newArr: Array<string> = []
for (const r of resList) {
if (r[dataFormat[0].key]) {
newArr.push(
dataFormat
?.map((format: { key: string; hideName: boolean }) => {
return format?.hideName ? r[format.key] : `${format.key}=${r[format.key]}`
})
.join(' ')
)
}
}
const addLine = (index: number) => {
resList.splice(index + 1, 0, ...getDefaultListItem())
const newKvList = [...resList]
setResList(newKvList)
emitNewArr()
}
const removeLine = (index: number) => {
resList.splice(index, 1)
const newKvList = [...resList]
setResList([...newKvList])
emitNewArr()
}
return (
<div>
{resList?.map((n: unknown, index: unknown) => {
return (
<div
key={n + index}
className="flex"
style={{ marginTop: index === 0 ? '0px' : '16px' }}
>
{dataFormat?.map((data: unknown, index2: unknown) => {
return (
<Input
key={data.key + index2}
className="mr-[8px]"
style={{ width: data.width }}
value={n[data.key]}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, data.key, data)
}}
placeholder={data.placeholder || `请输入${data.key}`}
status={n.status}
type={data.type || 'text'}
/>
)
})}
{index !== resList.length - 1 && (
<div style={{ display: 'inline-block' }}>
{n[dataFormat[0].key] && (
<Icon icon="ic:baseline-add" onClick={() => addLine(index as unknown as number)} width="14" height="14"/>
)}
<Icon icon="ic:baseline-minus" onClick={() => removeLine(index as unknown as number)} width="14" height="14"/>
</div>
)}
</div>
)
})}
</div>
)
onChange(newArr)
}
)
const changeInputValue = (newValue: string, index: number, keyName: string, dataFormat: unknown) => {
const newArr = [...resList]
newArr[index][keyName] = newValue
newArr[index].status =
(dataFormat.required && !newValue) || (dataFormat.pattern && !dataFormat.pattern.test(newValue)) ? 'error' : ''
setResList(newArr)
emitNewArr()
if (index === resList.length - 1) {
setResList([...newArr, ...getDefaultListItem()])
}
}
const addLine = (index: number) => {
resList.splice(index + 1, 0, ...getDefaultListItem())
const newKvList = [...resList]
setResList(newKvList)
emitNewArr()
}
const removeLine = (index: number) => {
resList.splice(index, 1)
const newKvList = [...resList]
setResList([...newKvList])
emitNewArr()
}
return (
<div>
{resList?.map((n: unknown, index: unknown) => {
return (
<div key={n + index} className="flex" style={{ marginTop: index === 0 ? '0px' : '16px' }}>
{dataFormat?.map((data: unknown, index2: unknown) => {
return (
<Input
key={data.key + index2}
className="mr-[8px]"
style={{ width: data.width }}
value={n[data.key]}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, data.key, data)
}}
placeholder={data.placeholder || `请输入${data.key}`}
status={n.status}
type={data.type || 'text'}
/>
)
})}
{index !== resList.length - 1 && (
<div style={{ display: 'inline-block' }}>
{n[dataFormat[0].key] && (
<Icon
icon="ic:baseline-add"
onClick={() => addLine(index as unknown as number)}
width="14"
height="14"
/>
)}
<Icon
icon="ic:baseline-minus"
onClick={() => removeLine(index as unknown as number)}
width="14"
height="14"
/>
</div>
)}
</div>
)
})}
</div>
)
})
@@ -1,47 +1,35 @@
import {forwardRef, useImperativeHandle, useState} from 'react'
import { forwardRef, useImperativeHandle, useState } from 'react'
import { Codebox } from '@common/components/postcat/api/Codebox'
export const CustomCodeboxComponent = forwardRef(
(props: { [k: string]: unknown }, ref) => {
const {
mode = 'yaml',
theme = 'xcode',
fontSize,
height,
width = '100%',
onChange,
value
} = props
const [code, setCode] = useState(
mode === 'json' ? JSON.stringify(value) : value
)
useImperativeHandle(ref, () => ({}))
const handleChange = (value: string) => {
setCode(value)
let res = value
if (mode === 'json') {
try {
res = JSON.parse(value)
} catch {
console.warn(' 输入的json语句格式有误')
}
export const CustomCodeboxComponent = forwardRef((props: { [k: string]: unknown }, ref) => {
const { mode = 'yaml', theme = 'xcode', fontSize, height, width = '100%', onChange, value } = props
const [code, setCode] = useState(mode === 'json' ? JSON.stringify(value) : value)
useImperativeHandle(ref, () => ({}))
const handleChange = (value: string) => {
setCode(value)
let res = value
if (mode === 'json') {
try {
res = JSON.parse(value)
} catch {
console.warn(' 输入的json语句格式有误')
}
onChange(res)
}
return (
<div className=" mt-[4px] border-[1px] border-solid border-BORDER">
<Codebox
value={code}
language={mode}
enableToolbar={false}
theme={theme}
fontSize={fontSize}
height={height ?? 500}
width={width}
onChange={handleChange} />
</div>
)
onChange(res)
}
)
return (
<div className=" mt-[4px] border-[1px] border-solid border-BORDER">
<Codebox
value={code}
language={mode}
enableToolbar={false}
theme={theme}
fontSize={fontSize}
height={height ?? 500}
width={width}
onChange={handleChange}
/>
</div>
)
})
@@ -1,5 +1,4 @@
import {forwardRef,useImperativeHandle} from 'react'
import { forwardRef, useImperativeHandle } from 'react'
import { createSchemaField } from '@formily/react'
import {
FormItem,
@@ -82,57 +81,53 @@ const SchemaField = createSchemaField({
}
})
export const CustomDialogComponent = forwardRef(
(props: { [k: string]: unknown }, ref) => {
const { onChange, title, value, render } = props
useImperativeHandle(ref, () => ({}))
let editPage: boolean = false
try {
editPage = Object.keys(JSON.parse(JSON.stringify(value))).length > 0
} catch {}
export const CustomDialogComponent = forwardRef((props: { [k: string]: unknown }, ref) => {
const { onChange, title, value, render } = props
useImperativeHandle(ref, () => ({}))
let editPage: boolean = false
try {
editPage = Object.keys(JSON.parse(JSON.stringify(value))).length > 0
} catch {}
return (
<FormDialog.Portal>
<span
className="ant-formily-array-base-config"
onClick={() => {
const dialog = FormDialog(
editPage ? $t('编辑(0)',[title||'']) : $t('添加(0)',[title||'']),
() => {
return (
<FormLayout
// labelCol={6}
layout={'vertical'}
scrollToFirstError
name="CustomDialogComponent"
// wrapperCol={10}
form={value}>
<SchemaField schema={JSON.parse(render)} />
</FormLayout>
)
}
return (
<FormDialog.Portal>
<span
className="ant-formily-array-base-config"
onClick={() => {
const dialog = FormDialog(editPage ? $t('编辑(0)', [title || '']) : $t('添加(0)', [title || '']), () => {
return (
<FormLayout
// labelCol={6}
layout={'vertical'}
scrollToFirstError
name="CustomDialogComponent"
// wrapperCol={10}
form={value}
>
<SchemaField schema={JSON.parse(render)} />
</FormLayout>
)
dialog
.forOpen((payload, next) => {
next({
initialValues: value
})
})
dialog
.forOpen((payload, next) => {
next({
initialValues: value
})
.forConfirm((payload, next) => {
next(payload)
})
.forCancel((payload, next) => {
next(payload)
})
.open()
.then(onChange)
}}
>
<svg style={{ width: '16px', height: '16px' }}>
<use href="#tool"></use>
</svg>
</span>
</FormDialog.Portal>
)
}
)
})
.forConfirm((payload, next) => {
next(payload)
})
.forCancel((payload, next) => {
next(payload)
})
.open()
.then(onChange)
}}
>
<svg style={{ width: '16px', height: '16px' }}>
<use href="#tool"></use>
</svg>
</span>
</FormDialog.Portal>
)
})
@@ -1,104 +1,99 @@
import {forwardRef, useImperativeHandle, useState} from 'react'
import { forwardRef, useImperativeHandle, useState } from 'react'
import { Input } from '@formily/antd-v5'
import { $t } from '@common/locales'
import { Icon } from '@iconify/react/dist/iconify.js'
export const SimpleMapComponent = forwardRef(
(props: { [k: string]: unknown }, ref) => {
const {
onChange,
value,
placeholderKey = $t('请输入Key'),
placeholderValue = $t('请输入Value')
} = props
export const SimpleMapComponent = forwardRef((props: { [k: string]: unknown }, ref) => {
const { onChange, value, placeholderKey = $t('请输入Key'), placeholderValue = $t('请输入Value') } = props
const [kvList, setKvList] = useState(
value && Object.keys(value).length > 0
? [
...Object.keys(value)?.map((k: string) => {
return { key: k, value: value[k] }
}),
{ key: '', value: '' }
]
: [{ key: '', value: '' }]
)
const [kvList, setKvList] = useState(
value && Object.keys(value).length > 0
? [
...Object.keys(value)?.map((k: string) => {
return { key: k, value: value[k] }
}),
{ key: '', value: '' }
]
: [{ key: '', value: '' }]
)
useImperativeHandle(ref, () => ({}))
useImperativeHandle(ref, () => ({}))
const emitNewArr = () => {
const res: { [k: string]: unknown } = {}
for (const kv of kvList) {
res[kv.key] = kv.value
}
onChange(res)
const emitNewArr = () => {
const res: { [k: string]: unknown } = {}
for (const kv of kvList) {
res[kv.key] = kv.value
}
const changeInputValue = (
newValue: string,
index: number,
type: 'key' | 'value'
) => {
const newArr = [...kvList]
newArr[index][type] = newValue
setKvList(newArr)
emitNewArr()
if (index === kvList.length - 1) {
setKvList([...newArr, { key: '', value: '' }])
}
}
const addLine = (index: number) => {
kvList.splice(index + 1, 0, { key: '', value: '' })
const newKvList = [...kvList]
setKvList(newKvList)
emitNewArr()
}
const removeLine = (index: number) => {
kvList.splice(index, 1)
const newKvList = [...kvList]
setKvList(newKvList)
emitNewArr()
}
return (
<div>
{kvList?.map((n: unknown, index: unknown) => {
return (
<div
key={n + index}
className="flex"
style={{ marginTop: index === 0 ? '0px' : '16px' }}
>
<Input
className="w-INPUT_NORMAL mr-[8px]"
style={{ width: '174px' }}
value={n.key}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, 'key')
}}
placeholder={placeholderKey}
/>
<Input
style={{ width: '164px', marginRight: '10px' }}
value={n.value}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, 'value')
}}
placeholder={placeholderValue}
/>
{index !== kvList.length - 1 && (
<div style={{ display: 'inline-block' }}>
{n.key && (
<Icon icon="ic:baseline-add" onClick={() => addLine(index as unknown as number)} width="14" height="14"/>
)}
<Icon icon="ic:baseline-minus" onClick={() => removeLine(index as unknown as number)} width="14" height="14"/>
</div>
)}
</div>
)
})}
</div>
)
onChange(res)
}
)
const changeInputValue = (newValue: string, index: number, type: 'key' | 'value') => {
const newArr = [...kvList]
newArr[index][type] = newValue
setKvList(newArr)
emitNewArr()
if (index === kvList.length - 1) {
setKvList([...newArr, { key: '', value: '' }])
}
}
const addLine = (index: number) => {
kvList.splice(index + 1, 0, { key: '', value: '' })
const newKvList = [...kvList]
setKvList(newKvList)
emitNewArr()
}
const removeLine = (index: number) => {
kvList.splice(index, 1)
const newKvList = [...kvList]
setKvList(newKvList)
emitNewArr()
}
return (
<div>
{kvList?.map((n: unknown, index: unknown) => {
return (
<div key={n + index} className="flex" style={{ marginTop: index === 0 ? '0px' : '16px' }}>
<Input
className="w-INPUT_NORMAL mr-[8px]"
style={{ width: '174px' }}
value={n.key}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, 'key')
}}
placeholder={placeholderKey}
/>
<Input
style={{ width: '164px', marginRight: '10px' }}
value={n.value}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, 'value')
}}
placeholder={placeholderValue}
/>
{index !== kvList.length - 1 && (
<div style={{ display: 'inline-block' }}>
{n.key && (
<Icon
icon="ic:baseline-add"
onClick={() => addLine(index as unknown as number)}
width="14"
height="14"
/>
)}
<Icon
icon="ic:baseline-minus"
onClick={() => removeLine(index as unknown as number)}
width="14"
height="14"
/>
</div>
)}
</div>
)
})}
</div>
)
})
@@ -1,327 +1,333 @@
import {forwardRef, useEffect, useImperativeHandle, useMemo, useState} from "react";
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
import { action } from '@formily/reactive'
import {
FormItem,
Space,
ArrayItems,
DatePicker,
Editable,
FormButtonGroup,
Input,
Radio,
Select,
Submit,
Cascader,
Form,
FormGrid,
FormLayout,
Upload,
FormItem,
Space,
ArrayItems,
DatePicker,
Editable,
FormButtonGroup,
Input,
Radio,
Select,
Submit,
Cascader,
Form,
FormGrid,
FormLayout,
Upload,
ArrayCollapse,
ArrayTable,
ArrayTabs,
Checkbox,
FormCollapse,
FormDialog,
FormDrawer,
FormStep,
FormTab,
NumberPicker,
Password,
PreviewText,
Reset,
SelectTable,
Switch,
TimePicker,
Transfer,
TreeSelect,
ArrayCards
} from '@formily/antd-v5'
import { createForm } from '@formily/core'
import { CustomCodeboxComponent } from '@common/components/aoplatform/formily2-customize/CustomCodeboxComponent.tsx'
import { SimpleMapComponent } from '@common/components/aoplatform/formily2-customize/SimpleMapComponent.tsx'
import { CustomDialogComponent } from '@common/components/aoplatform/formily2-customize/CustomDialogComponent.tsx'
import { ArrayItemBlankComponent } from '@common/components/aoplatform/formily2-customize/ArrayItemBlankComponent.tsx'
import { DefaultOptionType } from 'antd/es/cascader'
import { createSchemaField, FormProvider, RecursionField, useField, useForm } from '@formily/react'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import { useFetch } from '@common/hooks/http.ts'
import { App } from 'antd'
import { $t } from '@common/locales'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { setValidateLanguage } from '@formily/core'
export const DynamicRender = (props) => {
const { schema } = props
const field = useField()
const form = useForm()
const [renderSchema, setRenderSchema] = useState({})
const { state } = useGlobalContext()
useEffect(() => {
form.clearFormGraph(`${field.address}.*`)
try {
const parsedSchema = JSON.parse(schema)
setRenderSchema(parsedSchema[form?.values?.driver])
} catch (e) {
console.error('渲染出错', e?.message)
}
}, [form.values.driver])
const translateSchema = (render) => {
const res1 = {
...render,
...(render.title ? { title: $t(render.title) } : {}),
...(render.description ? { description: $t(render.description) } : {}),
...(render.label ? { label: $t(render.label) } : {}),
...(render.properties
? {
properties: Object.keys(render.properties).reduce((total, cur) => {
try {
total[cur] = translateSchema(render.properties[cur])
} catch (error) {
console.error(`Error translating schema for property ${cur}:`, error)
}
return total
}, {})
}
: {}),
...(render.items && Array.isArray(render.items) ? { items: render.items.map((x) => translateSchema(x)) } : {}),
...(render.items && !Array.isArray(render.items) ? { items: translateSchema(render.items) } : {}),
...(render.additionalProperties ? { additionalProperties: translateSchema(render.additionalProperties) } : {}),
...(render.enum ? { enum: render.enum.map((x) => ({ ...x, label: $t(x.label) })) } : {})
}
return res1
}
const translatedRenderSchema = useMemo(() => {
const res = renderSchema && translateSchema(renderSchema)
return res
}, [state.language, renderSchema])
return <RecursionField basePath={field.address} schema={translatedRenderSchema} onlyRenderProperties />
}
export type IntelligentPluginConfigProps = {
type: 'add' | 'edit'
renderSchema: unknown
tabData: DefaultOptionType[]
moduleId: string
driverSelectionOptions: DefaultOptionType[]
entityId?: string
initFormValue: { [k: string]: unknown }
}
export type IntelligentPluginConfigHandle = {
save: () => Promise<boolean | string>
}
const SchemaField = createSchemaField({
components: {
ArrayCards,
ArrayCollapse,
ArrayItems,
ArrayTable,
ArrayTabs,
Cascader,
Checkbox,
DatePicker,
Editable,
Form,
FormButtonGroup,
FormCollapse,
// @ts-ignore
FormDialog,
// @ts-ignore
FormDrawer,
FormGrid,
FormItem,
FormLayout,
FormStep,
FormTab,
Input,
NumberPicker,
Password,
PreviewText,
Radio,
Reset,
Select,
SelectTable,
Space,
Submit,
Switch,
TimePicker,
Transfer,
TreeSelect,
ArrayCards
} from '@formily/antd-v5'
import { createForm } from '@formily/core'
import {CustomCodeboxComponent} from "@common/components/aoplatform/formily2-customize/CustomCodeboxComponent.tsx";
import {SimpleMapComponent} from "@common/components/aoplatform/formily2-customize/SimpleMapComponent.tsx";
import {CustomDialogComponent} from "@common/components/aoplatform/formily2-customize/CustomDialogComponent.tsx";
import {ArrayItemBlankComponent} from "@common/components/aoplatform/formily2-customize/ArrayItemBlankComponent.tsx";
import {DefaultOptionType} from "antd/es/cascader";
import {createSchemaField, FormProvider, RecursionField, useField, useForm} from "@formily/react";
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import {App, Descriptions} from "antd";
import { $t } from "@common/locales";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { setValidateLanguage } from '@formily/core'
export const DynamicRender = (props) => {
const {schema} = props
const field = useField()
const form = useForm()
const [renderSchema, setRenderSchema] = useState({})
const {state} = useGlobalContext()
useEffect(() => {
form.clearFormGraph(`${field.address}.*`)
try{
const parsedSchema = JSON.parse(schema)
setRenderSchema(parsedSchema[form?.values?.driver])
}catch(e){
console.error('渲染出错',e?.message)
}
}, [form.values.driver])
const translateSchema = (render) =>{
const res1 = {
...render,
...(render.title ? {title:$t(render.title)} : {}),
...(render.description) ? {description:$t(render.description)} : {},
...(render.label ? {label:$t(render.label)} : {}),
...(render.properties ? {properties: Object.keys(render.properties).reduce((total, cur) => {
try {
total[cur] = translateSchema(render.properties[cur]);
} catch (error) {
console.error(`Error translating schema for property ${cur}:`, error);
}
return total;
}, {})} : {}),
...(render.items && Array.isArray(render.items) ? {items:render.items.map(x=>translateSchema(x))} : {}),
...(render.items && !Array.isArray(render.items) ? {items:translateSchema(render.items)} : {}),
...(render.additionalProperties ? {additionalProperties: translateSchema(render.additionalProperties)} : {}),
...(render.enum ? {enum: render.enum.map(x=>({...x, label:$t(x.label)}))} : {}),
}
return res1
}
const translatedRenderSchema = useMemo(()=>{
const res = renderSchema && translateSchema(renderSchema)
return res
},[state.language,renderSchema])
return (
<RecursionField
basePath={field.address}
schema={translatedRenderSchema}
onlyRenderProperties
/>
)
}
export type IntelligentPluginConfigProps = {
type:'add'|'edit'
renderSchema:unknown
tabData:DefaultOptionType[]
moduleId:string
driverSelectionOptions:DefaultOptionType[]
entityId?:string
initFormValue:{[k:string]:unknown}
}
export type IntelligentPluginConfigHandle = {
save:()=>Promise<boolean | string>
}
const SchemaField = createSchemaField({
components: {
ArrayCards,
ArrayCollapse,
ArrayItems,
ArrayTable,
ArrayTabs,
Cascader,
Checkbox,
DatePicker,
Editable,
Form,
FormButtonGroup,
FormCollapse,
// @ts-ignore
FormDialog,
// @ts-ignore
FormDrawer,
FormGrid,
FormItem,
FormLayout,
FormStep,
FormTab,
Input,
NumberPicker,
Password,
PreviewText,
Radio,
Reset,
Select,
SelectTable,
Space,
Submit,
Switch,
TimePicker,
Transfer,
TreeSelect,
Upload,
CustomCodeboxComponent,
SimpleMapComponent,
CustomDialogComponent,
ArrayItemBlankComponent,
DynamicRender
}
Upload,
CustomCodeboxComponent,
SimpleMapComponent,
CustomDialogComponent,
ArrayItemBlankComponent,
DynamicRender
}
})
export const IntelligentPluginConfig = forwardRef<IntelligentPluginConfigHandle,IntelligentPluginConfigProps>((props,ref)=>{
const { type,renderSchema,moduleId,driverSelectionOptions,initFormValue} = props
export const IntelligentPluginConfig = forwardRef<IntelligentPluginConfigHandle, IntelligentPluginConfigProps>(
(props, ref) => {
const { type, renderSchema, moduleId, driverSelectionOptions, initFormValue } = props
const { message } = App.useApp()
const {fetchData} = useFetch()
const { fetchData } = useFetch()
const form = createForm({ validateFirst: type === 'edit' })
form.setInitialValues(initFormValue || {})
const { state } = useGlobalContext()
useEffect(()=>{
setValidateLanguage(state.language)
},[state.language])
useEffect(() => {
setValidateLanguage(state.language)
}, [state.language])
const pluginEditSchema = {
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
labelCol: 6,
wrapperCol: 10,
layout: 'vertical',
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
labelCol: 6,
wrapperCol: 10,
layout: 'vertical'
},
properties: {
id: {
type: 'string',
title: $t('ID'),
required: true,
pattern: /^[a-zA-Z][a-zA-Z0-9-_]*$/,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 4,
wrapperCol: 20,
labelAlign: 'left'
},
properties: {
id: {
type: 'string',
title: $t('ID'),
required: true,
pattern: /^[a-zA-Z][a-zA-Z0-9-_]*$/,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol:4,
wrapperCol: 20,
labelAlign:'left'
},
'x-component': 'Input',
'x-component-props': {
placeholder: $t(PLACEHOLDER.specialStartWithAlphabet),
},
'x-disabled': type === 'edit'
},
title: {
type: 'string',
title: $t('名称'),
required: true,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol:4,
wrapperCol: 20,
labelAlign:'left'
},
'x-component': 'Input',
'x-component-props': {
placeholder: $t(PLACEHOLDER.input),
}
},
driver: {
type: 'string',
title: $t('Driver'),
required: true,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol:4,
wrapperCol: 20,
labelAlign:'left'
},
'x-component': 'Select',
'x-component-props': {
disabled: type === 'edit'
},
'x-display': driverSelectionOptions.length > 1 ? 'visible' : 'hidden',
enum: [...driverSelectionOptions]
},
description: {
type: 'string',
title: $t('描述'),
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol:4,
wrapperCol: 20,
labelAlign:'left'
},
'x-component': 'Input.TextArea',
'x-component-props': {
placeholder: $t(PLACEHOLDER.input),
}
},
config: {
type: 'object',
'x-component': 'DynamicRender',
'x-component-props': {
schema: JSON.stringify(renderSchema),
}
}
'x-component': 'Input',
'x-component-props': {
placeholder: $t(PLACEHOLDER.specialStartWithAlphabet)
},
'x-disabled': type === 'edit'
},
title: {
type: 'string',
title: $t('名称'),
required: true,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 4,
wrapperCol: 20,
labelAlign: 'left'
},
'x-component': 'Input',
'x-component-props': {
placeholder: $t(PLACEHOLDER.input)
}
},
driver: {
type: 'string',
title: $t('Driver'),
required: true,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 4,
wrapperCol: 20,
labelAlign: 'left'
},
'x-component': 'Select',
'x-component-props': {
disabled: type === 'edit'
},
'x-display': driverSelectionOptions.length > 1 ? 'visible' : 'hidden',
enum: [...driverSelectionOptions]
},
description: {
type: 'string',
title: $t('描述'),
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 4,
wrapperCol: 20,
labelAlign: 'left'
},
'x-component': 'Input.TextArea',
'x-component-props': {
placeholder: $t(PLACEHOLDER.input)
}
},
config: {
type: 'object',
'x-component': 'DynamicRender',
'x-component-props': {
schema: JSON.stringify(renderSchema)
}
}
}
}
}
}
const save :()=>Promise<boolean | string> = ()=>{
return new Promise((resolve, reject)=>{
form.validate().then(()=>{
fetchData<BasicResponse<null>>(type === 'add'?`dynamic/${moduleId}`:`dynamic/${moduleId}/config`,{method:type === 'add'? 'POST' : 'PUT',eoBody:form.values, eoParams:{...(type !== 'add' && {id:initFormValue.id})}}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo:unknown)=> reject(errorInfo))
})
}
}
useImperativeHandle(ref, ()=>({
save
})
)
const save: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
form
.validate()
.then(() => {
fetchData<BasicResponse<null>>(type === 'add' ? `dynamic/${moduleId}` : `dynamic/${moduleId}/config`, {
method: type === 'add' ? 'POST' : 'PUT',
eoBody: form.values,
eoParams: { ...(type !== 'add' && { id: initFormValue.id }) }
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo: unknown) => reject(errorInfo))
})
}
useImperativeHandle(ref, () => ({
save
}))
const getSkillData = async (skill: string) => {
return new Promise((resolve,reject) => {
fetchData<BasicResponse<{[k:string]:Array<{name:string,title:string}>}>>(`api/common/provider/${skill}`,{method:'GET'}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
resolve(data[skill]?.map((x:{name:string,title:string})=>{return{label:x.title, value:x.name}}) || [])
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
return new Promise((resolve, reject) => {
fetchData<BasicResponse<{ [k: string]: Array<{ name: string; title: string }> }>>(
`api/common/provider/${skill}`,
{ method: 'GET' }
).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
resolve(
data[skill]?.map((x: { name: string; title: string }) => {
return { label: x.title, value: x.name }
}) || []
)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
})
}
const useAsyncDataSource =
(service: unknown, skill: string) => (field: unknown) => {
field.loading = true
service(skill).then(
action.bound &&
action.bound((data: unknown) => {
field.dataSource = data
field.loading = false
})
)
}
const useAsyncDataSource = (service: unknown, skill: string) => (field: unknown) => {
field.loading = true
service(skill).then(
action.bound &&
action.bound((data: unknown) => {
field.dataSource = data
field.loading = false
})
)
}
return (
<div className="pl-[12px]">
<FormProvider form={form} >
<SchemaField
schema={pluginEditSchema}
scope={{ useAsyncDataSource, getSkillData, form }}
/>
</FormProvider>
</div>)
})
<div className="pl-[12px]">
<FormProvider form={form}>
<SchemaField schema={pluginEditSchema} scope={{ useAsyncDataSource, getSkillData, form }} />
</FormProvider>
</div>
)
}
)
@@ -1,349 +1,434 @@
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx";
import {App, Divider, Spin} from "antd";
import {useEffect, useMemo, useRef, useState} from "react";
import { useLocation, useOutletContext, useParams} from "react-router-dom";
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {ActionType, ParamsType} from "@ant-design/pro-components";
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
import {DefaultOptionType} from "antd/es/cascader";
import {IntelligentPluginConfig, IntelligentPluginConfigHandle} from "./IntelligentPluginConfig.tsx";
import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import {EntityItem} from "@common/const/type.ts";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx";
import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx";
import { LoadingOutlined } from "@ant-design/icons";
import { $t } from "@common/locales/index.ts";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { LoadingOutlined } from '@ant-design/icons'
import { ActionType, ParamsType } from '@ant-design/pro-components'
import { DrawerWithFooter } from '@common/components/aoplatform/DrawerWithFooter.tsx'
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList.tsx'
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx'
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import { EntityItem } from '@common/const/type.ts'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import { useFetch } from '@common/hooks/http.ts'
import { $t } from '@common/locales/index.ts'
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
import { App, Divider, Spin } from 'antd'
import { DefaultOptionType } from 'antd/es/cascader'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useLocation, useOutletContext, useParams } from 'react-router-dom'
import { IntelligentPluginConfig, IntelligentPluginConfigHandle } from './IntelligentPluginConfig.tsx'
type DynamicTableField = {
name: string,
title: string,
attr: string,
enum: Array<string>
type DynamicTableField = {
name: string
title: string
attr: string
enum: Array<string>
}
type DynamicDriverData = {
name:string, title:string
type DynamicDriverData = {
name: string
title: string
}
export type DynamicTableConfig = {
basic:{
id:string,
name: string,
title: string,
drivers: Array<DynamicDriverData>,
fields: Array<DynamicTableField>,
}
list: Array<DynamicTableItem>,
total:number
basic: {
id: string
name: string
title: string
drivers: Array<DynamicDriverData>
fields: Array<DynamicTableField>
}
list: Array<DynamicTableItem>
total: number
}
export type DynamicRender = {
render:unknown,
basic:{
id:string,
name:string,
title:string
}
render: unknown
basic: {
id: string
name: string
title: string
}
}
export type DynamicPublishCluster = {
name:string,
title:string,
status:string,
updater:EntityItem,
update_time:string,
checked?:boolean
name: string
title: string
status: string
updater: EntityItem
update_time: string
checked?: boolean
}
export type DynamicPublishData = {
id:string,
name:string,
title:string,
description:string
clusters:DynamicPublishCluster[]
id: string
name: string
title: string
description: string
clusters: DynamicPublishCluster[]
}
export type DynamicTableItem = {[k:string]:unknown}
export type DynamicTableItem = { [k: string]: unknown }
export const StatusColorClass = {
"已发布":'text-[#03a9f4]',
"待发布":'text-[#46BE11]',
"未发布":'text-[#03a9f4]'
: 'text-[#03a9f4]',
: 'text-[#46BE11]',
: 'text-[#03a9f4]'
}
export type DynamicPublish = {
code:number,
msg:string,
data:{
success:Array<string>,
fail:Array<string>
}
code: number
msg: string
data: {
success: Array<string>
fail: Array<string>
}
}
export default function IntelligentPluginList(){
const { modal,message } = App.useApp()
const [searchWord, setSearchWord] = useState<string>('')
const { moduleId } = useParams<RouterParams>();
const [pluginName,setPluginName] = useState<string>('-')
const [partitionOptions] = useState<DefaultOptionType[]>([{label:'default', value:'default'}])
const { setBreadcrumb } = useBreadcrumb()
const [renderSchema ,setRenderSchema] = useState<{[k:string]:unknown}>({})
const drawerFormRef = useRef<IntelligentPluginConfigHandle>(null);
const [driverOptions, setDriverOptions] = useState<DefaultOptionType[]>([])
const [tableListDataSource, setTableListDataSource] = useState<DynamicTableItem[]>([]);
export default function IntelligentPluginList() {
const { modal, message } = App.useApp()
const [searchWord, setSearchWord] = useState<string>('')
const { moduleId } = useParams<RouterParams>()
const [pluginName, setPluginName] = useState<string>('-')
const [partitionOptions] = useState<DefaultOptionType[]>([{ label: 'default', value: 'default' }])
const { setBreadcrumb } = useBreadcrumb()
const [renderSchema, setRenderSchema] = useState<{ [k: string]: unknown }>({})
const drawerFormRef = useRef<IntelligentPluginConfigHandle>(null)
const [driverOptions, setDriverOptions] = useState<DefaultOptionType[]>([])
const [tableListDataSource, setTableListDataSource] = useState<DynamicTableItem[]>([])
const [tableHttpReload, setTableHttpReload] = useState(true);
const [columns,setColumns] = useState<DynamicTableField[] >([])
const {fetchData} = useFetch()
const pageListRef = useRef<ActionType>(null);
const [publishBtnLoading, setPublishBtnLoading] = useState<boolean>(false)
const [curDetail,setCurDetail] = useState<{[k: string]: unknown;}|undefined>()
const [drawerType, setDrawerType] = useState<'add'|'edit'>('add')
const [drawerOpen, setDrawerOpen] = useState<boolean>(false)
const [drawerLoading, setDrawerLoading] = useState<boolean>(false)
const location = useLocation().pathname
const {accessPrefix} = useOutletContext<{accessPrefix:string}>()
const {state} = useGlobalContext()
const [tableHttpReload, setTableHttpReload] = useState(true)
const [columns, setColumns] = useState<DynamicTableField[]>([])
const { fetchData } = useFetch()
const pageListRef = useRef<ActionType>(null)
const [publishBtnLoading, setPublishBtnLoading] = useState<boolean>(false)
const [curDetail, setCurDetail] = useState<{ [k: string]: unknown } | undefined>()
const [drawerType, setDrawerType] = useState<'add' | 'edit'>('add')
const [drawerOpen, setDrawerOpen] = useState<boolean>(false)
const [drawerLoading, setDrawerLoading] = useState<boolean>(false)
const location = useLocation().pathname
const { accessPrefix } = useOutletContext<{ accessPrefix: string }>()
const { state } = useGlobalContext()
const getIntelligentPluginTableList=(params:ParamsType & {
pageSize?: number | undefined;
current?: number | undefined;
keyword?: string | undefined;
}): Promise<{ data: DynamicTableItem[], success: boolean }>=> {
if(!tableHttpReload){
setTableHttpReload(true)
return Promise.resolve({
data: tableListDataSource,
success: true,
});
const getIntelligentPluginTableList = (
params: ParamsType & {
pageSize?: number | undefined
current?: number | undefined
keyword?: string | undefined
}
): Promise<{ data: DynamicTableItem[]; success: boolean }> => {
if (!tableHttpReload) {
setTableHttpReload(true)
return Promise.resolve({
data: tableListDataSource,
success: true
})
}
const query = {
page: params.current,
pageSize: params.pageSize,
keyword: searchWord
}
return fetchData<BasicResponse<DynamicTableConfig>>(`dynamic/${moduleId}/list`, {
method: 'GET',
eoParams: query,
eoTransformKeys: ['pageSize']
})
.then((res) => {
message.destroy()
if (res.code === STATUS_CODE.SUCCESS) {
getConfig(res.data)
setColumns(res.data.basic.fields)
setTableListDataSource(res.data.list)
return { data: res.data.list, success: true, total: res.data.total }
} else {
setTableListDataSource([])
return { data: [], success: false }
}
const query = {
page:params.current,
pageSize:params.pageSize,
keyword:searchWord,
}
return fetchData<BasicResponse<DynamicTableConfig>>(
`dynamic/${moduleId}/list`,
{method:'GET',eoParams:query,eoTransformKeys:['pageSize']}).then((res)=>{
message.destroy();
if(res.code === STATUS_CODE.SUCCESS){
getConfig(res.data)
setColumns(res.data.basic.fields)
setTableListDataSource(res.data.list);
return ({ data: res.data.list, success: true,total:res.data.total });
}else{
setTableListDataSource([]);
return ({ data: [], success: false });
}
}).catch((e)=>{console.warn(e);
return ({ data: [], success: false });})
}
})
.catch((e) => {
console.warn(e)
return { data: [], success: false }
})
}
const translatedCol = useMemo(()=>columns.map((field:DynamicTableField, index:number)=>({
title: typeof field.title === 'string' ? $t(field.title as string): field.title,
dataIndex:field.name,
fixed:field.name === 'title' ? 'left' : undefined,
ellipsis:true,
width:field.name === 'title' ? 150 : undefined,
...(field.enum?.length > 0 ?{
onFilter: (value: string, record: { [x: string]: string | string[]; }) => record[field.name].indexOf(value) === 0,
filters:field.enum?.map((x:string)=>{return {text:$t(x), value:x}}),
render:(_: unknown, entity: { [x: string]: string; })=> {
return <span className={StatusColorClass[entity[field.name] as keyof typeof StatusColorClass]}>{$t(entity[field.name] as string)}</span>
},
}:{}),
})),[state.language,columns])
const getConfig = (data:DynamicTableConfig)=>{
const {basic,list } = data
const {title,drivers} = basic
setBreadcrumb([
{title:location.includes('resourcesettings') ? $t('资源'): $t('日志')},
{
title
}
])
setPluginName(title)
setDriverOptions(drivers?.map((driver:DynamicDriverData) => {
return { label: driver.title, value: driver.name }
}) || [])
}
const getRender = ()=>{
return fetchData<BasicResponse<DynamicRender>>(`dynamic/${moduleId}/render`,{method:'GET'}).then((resp) => {
if (resp.code === STATUS_CODE.SUCCESS) {
setRenderSchema(resp.data.render)
return Promise.resolve(resp.data.render)
}
return Promise.reject(resp.msg || $t(RESPONSE_TIPS.error))
})
}
const operation:PageProColumns<DynamicTableItem>[] =[
{
title: COLUMNS_TITLE.operate,
key: 'option',
fixed:'right',
valueType: 'option',
btnNums:3,
render: (_: React.ReactNode, entity: DynamicTableItem) => [
<TableBtnWithPermission access={`${accessPrefix}.publish`} key="publish" btnType="publish" onClick={()=>{openModal('publish',entity)}} btnTitle={entity.status === $t('已发布') ? $t('下线') : $t('上线')}/>,
<Divider type="vertical" className="mx-0" key="div1"/>,
<TableBtnWithPermission access={`${accessPrefix}.view`} key="edit" btnType="edit" onClick={()=>{openDrawer('edit',entity)}} btnTitle={$t("查看")}/>,
<Divider type="vertical" className="mx-0" key="div2"/>,
<TableBtnWithPermission access={`${accessPrefix}.delete`} key="delete" btnType="delete" onClick={()=>{openModal('delete',entity)}} btnTitle={$t("删除")}/>,
],
}
]
const handleClusterChange = (e:string[])=>{
setTableHttpReload(true)
pageListRef.current?.reload()
}
const manualReloadTable = () => {
setTableHttpReload(true); // 表格数据需要从后端接口获取
pageListRef.current?.reload()
};
const deleteInstance = (entity:DynamicTableItem)=>{
return new Promise((resolve, reject)=>{
fetchData<BasicResponse<null>>(`dynamic/${moduleId}/batch`,{method:'DELETE',eoParams:{ids:JSON.stringify([entity!.id])}}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
})
}
const openDrawer = async (type:'add'|'edit', entity?:DynamicTableItem)=>{
switch (type){
case 'add':
setCurDetail({driver:driverOptions[0].value || '',config:{}})
break;
case 'edit':{
setDrawerLoading(true)
fetchData<BasicResponse<{info:DynamicTableItem}>>(
`dynamic/${moduleId}/info`,
{method:'GET',eoParams:{id:entity!.id}}).then((res)=>{
const {code, data, msg } = res
if(code === STATUS_CODE.SUCCESS){
if(data.info.config){
}
setCurDetail(data.info)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).finally(()=>setDrawerLoading(false))
break;
}
}
setDrawerType(type)
setDrawerOpen(true)
}
const openModal = async (type:'publish'|'delete', entity?:DynamicTableItem)=>{
let title:string = ''
let content:string|React.ReactNode = ''
switch (type){
case 'publish':{
message.loading($t(RESPONSE_TIPS.operating))
await fetchData<BasicResponse<DynamicPublish>>(`dynamic/${moduleId}/${entity!.status === $t('已发布') ? 'offline':'online'}`, {
method: 'PUT',
eoParams:{id:entity!.id},
}).then(response => {
const {code, msg} = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
return Promise.resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> Promise.reject(errorInfo))
message.destroy()
return;}
case 'delete':
title='删除'
content=<span>{$t(DELETE_TIPS.default)}</span>
break;
}
modal.confirm({
title,
content,
onOk:()=>{
switch (type){ // case 'publish':
// return editRef.current?.save().then((res)=>{if(res === true) manualReloadTable()})
case 'delete':
return deleteInstance(entity!).then((res)=>{if(res === true) manualReloadTable()})
}
},
width: type === 'delete'? 600 : 900,
okText:$t('确认'),
okButtonProps:{
disabled:false
},
cancelText:$t('取消'),
closable:true,
icon:<></>,
footer:(_, { OkBtn, CancelBtn }) =>{
const translatedCol = useMemo(
() =>
columns.map((field: DynamicTableField, index: number) => ({
title: typeof field.title === 'string' ? $t(field.title as string) : field.title,
dataIndex: field.name,
fixed: field.name === 'title' ? 'left' : undefined,
ellipsis: true,
width: field.name === 'title' ? 150 : undefined,
...(field.enum?.length > 0
? {
onFilter: (value: string, record: { [x: string]: string | string[] }) =>
record[field.name].indexOf(value) === 0,
filters: field.enum?.map((x: string) => {
return { text: $t(x), value: x }
}),
render: (_: unknown, entity: { [x: string]: string }) => {
return (
<>
<WithPermission access=""><CancelBtn/></WithPermission>
<WithPermission access=""><OkBtn/></WithPermission>
</>
);
},
<span className={StatusColorClass[entity[field.name] as keyof typeof StatusColorClass]}>
{$t(entity[field.name] as string)}
</span>
)
}
}
: {})
})),
[state.language, columns]
)
const getConfig = (data: DynamicTableConfig) => {
const { basic, list } = data
const { title, drivers } = basic
setBreadcrumb([
{ title: location.includes('resourcesettings') ? $t('资源') : $t('日志') },
{
title
}
])
setPluginName(title)
setDriverOptions(
drivers?.map((driver: DynamicDriverData) => {
return { label: driver.title, value: driver.name }
}) || []
)
}
const getRender = () => {
return fetchData<BasicResponse<DynamicRender>>(`dynamic/${moduleId}/render`, { method: 'GET' }).then((resp) => {
if (resp.code === STATUS_CODE.SUCCESS) {
setRenderSchema(resp.data.render)
return Promise.resolve(resp.data.render)
}
return Promise.reject(resp.msg || $t(RESPONSE_TIPS.error))
})
}
const operation: PageProColumns<DynamicTableItem>[] = [
{
title: COLUMNS_TITLE.operate,
key: 'option',
fixed: 'right',
valueType: 'option',
btnNums: 3,
render: (_: React.ReactNode, entity: DynamicTableItem) => [
<TableBtnWithPermission
access={`${accessPrefix}.publish`}
key={entity.status === $t('已发布') ? 'offline' : 'publish'}
btnType={entity.status === $t('已发布') ? 'offline' : 'publish'}
onClick={() => {
openModal('publish', entity)
}}
btnTitle={entity.status === $t('已发布') ? $t('下线') : $t('上线')}
/>,
<Divider type="vertical" className="mx-0" key="div1" />,
<TableBtnWithPermission
access={`${accessPrefix}.view`}
key="edit"
btnType="edit"
onClick={() => {
openDrawer('edit', entity)
}}
btnTitle={$t('查看 ')}
/>,
<Divider type="vertical" className="mx-0" key="div2" />,
<TableBtnWithPermission
access={`${accessPrefix}.delete`}
key="delete"
btnType="delete"
onClick={() => {
openModal('delete', entity)
}}
btnTitle={$t('删除')}
/>
]
}
]
const handleClusterChange = (e: string[]) => {
setTableHttpReload(true)
pageListRef.current?.reload()
}
const manualReloadTable = () => {
setTableHttpReload(true) // 表格数据需要从后端接口获取
pageListRef.current?.reload()
}
const deleteInstance = (entity: DynamicTableItem) => {
return new Promise((resolve, reject) => {
fetchData<BasicResponse<null>>(`dynamic/${moduleId}/batch`, {
method: 'DELETE',
eoParams: { ids: JSON.stringify([entity!.id]) }
}).then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
})
}
const openDrawer = async (type: 'add' | 'edit', entity?: DynamicTableItem) => {
switch (type) {
case 'add':
setCurDetail({ driver: driverOptions[0].value || '', config: {} })
break
case 'edit': {
setDrawerLoading(true)
fetchData<BasicResponse<{ info: DynamicTableItem }>>(`dynamic/${moduleId}/info`, {
method: 'GET',
eoParams: { id: entity!.id }
})
.then((res) => {
const { code, data, msg } = res
if (code === STATUS_CODE.SUCCESS) {
if (data.info.config) {
}
setCurDetail(data.info)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.finally(() => setDrawerLoading(false))
break
}
}
setDrawerType(type)
setDrawerOpen(true)
}
const openModal = async (type: 'publish' | 'delete', entity?: DynamicTableItem) => {
let title: string = ''
let content: string | React.ReactNode = ''
switch (type) {
case 'publish': {
message.loading($t(RESPONSE_TIPS.operating))
await fetchData<BasicResponse<DynamicPublish>>(
`dynamic/${moduleId}/${entity!.status === $t('已发布') ? 'offline' : 'online'}`,
{
method: 'PUT',
eoParams: { id: entity!.id }
}
)
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
manualReloadTable()
return Promise.resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => Promise.reject(errorInfo))
return
}
case 'delete':
title = '删除'
content = <span>{$t(DELETE_TIPS.default)}</span>
break
}
useEffect(() => {
getRender()
pageListRef.current?.reload()
}, [moduleId]);
modal.confirm({
title,
content,
onOk: () => {
switch (
type // case 'publish':
) {
// return editRef.current?.save().then((res)=>{if(res === true) manualReloadTable()})
case 'delete':
return deleteInstance(entity!).then((res) => {
if (res === true) manualReloadTable()
})
}
},
width: type === 'delete' ? 600 : 900,
okText: $t('确认'),
okButtonProps: {
disabled: false
},
cancelText: $t('取消'),
closable: true,
icon: <></>,
footer: (_, { OkBtn, CancelBtn }) => {
return (
<>
<WithPermission access="">
<CancelBtn />
</WithPermission>
<WithPermission access="">
<OkBtn />
</WithPermission>
</>
)
}
})
}
useEffect(() => {
getRender()
pageListRef.current?.reload()
}, [moduleId])
return (<>
<PageList
ref={pageListRef}
columns = {[...translatedCol,...operation]}
request={(params)=>getIntelligentPluginTableList(params)}
addNewBtnTitle={$t('添加(0)',[$t(pluginName)])}
searchPlaceholder={$t('搜索(0)名称',[$t(pluginName)])}
onChange={() => {
setTableHttpReload(false)
}}
addNewBtnAccess={`${accessPrefix}.add`}
onAddNewBtnClick={()=>{openDrawer('add')}}
onSearchWordChange={(e)=>{setSearchWord(e.target.value);setTableHttpReload(true);setTableHttpReload(true)}}
/>
<DrawerWithFooter title={`${drawerType === 'add' ? $t('添加(0)',[$t(pluginName)]) : $t('编辑(0)',[$t(pluginName)])}`} open={drawerOpen} onClose={()=>{setCurDetail(undefined);setDrawerOpen(false)}} onSubmit={()=>drawerFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})} submitAccess=''>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin/>} spinning={drawerLoading}>
<IntelligentPluginConfig
ref={drawerFormRef!}
type={drawerType}
renderSchema={renderSchema}
tabData={partitionOptions}
moduleId={moduleId!}
driverSelectionOptions={driverOptions}
initFormValue={curDetail as { [k: string]: unknown; }} />
</Spin>
</DrawerWithFooter>
</>)
}
return (
<>
<PageList
ref={pageListRef}
columns={[...translatedCol, ...operation]}
request={(params) => getIntelligentPluginTableList(params)}
addNewBtnTitle={$t('添加(0)', [$t(pluginName)])}
searchPlaceholder={$t('搜索(0)名称', [$t(pluginName)])}
onChange={() => {
setTableHttpReload(false)
}}
addNewBtnAccess={`${accessPrefix}.add`}
onAddNewBtnClick={() => {
openDrawer('add')
}}
onSearchWordChange={(e) => {
setSearchWord(e.target.value)
setTableHttpReload(true)
setTableHttpReload(true)
}}
/>
<DrawerWithFooter
title={`${drawerType === 'add' ? $t('添加(0)', [$t(pluginName)]) : $t('编辑(0)', [$t(pluginName)])}`}
open={drawerOpen}
onClose={() => {
setCurDetail(undefined)
setDrawerOpen(false)
}}
onSubmit={() =>
drawerFormRef.current?.save()?.then((res) => {
res && manualReloadTable()
return res
})
}
submitAccess=""
>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={drawerLoading}>
<IntelligentPluginConfig
ref={drawerFormRef!}
type={drawerType}
renderSchema={renderSchema}
tabData={partitionOptions}
moduleId={moduleId!}
driverSelectionOptions={driverOptions}
initFormValue={curDetail as { [k: string]: unknown }}
/>
</Spin>
</DrawerWithFooter>
</>
)
}
@@ -1,14 +1,9 @@
'use client'
import type { FC } from 'react'
import { useEffect, useMemo } from 'react'
import type {
EditorState,
} from 'lexical'
import {
$getRoot,
TextNode,
} from 'lexical'
import { useEffect } from 'react'
import type { EditorState } from 'lexical'
import { $getRoot, TextNode } from 'lexical'
import { CodeNode } from '@lexical/code'
import { LexicalComposer } from '@lexical/react/LexicalComposer'
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
@@ -19,25 +14,13 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
// import TreeView from './plugins/tree-view'
import Placeholder from './plugins/placeholder'
import ComponentPickerBlock from './plugins/component-picker-block/index'
import {
ContextBlock,
ContextBlockNode,
ContextBlockReplacementBlock,
} from './plugins/context-block/index'
import {
QueryBlock,
QueryBlockNode,
QueryBlockReplacementBlock,
} from './plugins/query-block/index'
import {
HistoryBlock,
HistoryBlockNode,
HistoryBlockReplacementBlock,
} from './plugins/history-block/index'
import { ContextBlock, ContextBlockNode, ContextBlockReplacementBlock } from './plugins/context-block/index'
import { QueryBlock, QueryBlockNode, QueryBlockReplacementBlock } from './plugins/query-block/index'
import { HistoryBlock, HistoryBlockNode, HistoryBlockReplacementBlock } from './plugins/history-block/index'
import {
WorkflowVariableBlock,
WorkflowVariableBlockNode,
WorkflowVariableBlockReplacementBlock,
WorkflowVariableBlockReplacementBlock
} from './plugins/workflow-variable-block/index'
import VariableBlock from './plugins/variable-block/index'
import VariableValueBlock from './plugins/variable-value-block/index'
@@ -52,12 +35,9 @@ import type {
HistoryBlockType,
QueryBlockType,
VariableBlockType,
WorkflowVariableBlockType,
WorkflowVariableBlockType
} from './types'
import {
UPDATE_DATASETS_EVENT_EMITTER,
UPDATE_HISTORY_EVENT_EMITTER,
} from './constants'
import { UPDATE_DATASETS_EVENT_EMITTER, UPDATE_HISTORY_EVENT_EMITTER } from './constants'
import { useEventEmitterContextContext } from '@common/contexts/EventEmitterContext'
export type PromptEditorProps = {
@@ -97,7 +77,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
historyBlock,
variableBlock,
externalToolBlock,
workflowVariableBlock,
workflowVariableBlock
}) => {
const { eventEmitter } = useEventEmitterContextContext()
const initialConfig = {
@@ -107,51 +87,58 @@ const PromptEditor: FC<PromptEditorProps> = ({
CustomTextNode,
{
replace: TextNode,
with: (node: TextNode) => new CustomTextNode(node.__text),
with: (node: TextNode) => new CustomTextNode(node.__text)
},
ContextBlockNode,
HistoryBlockNode,
QueryBlockNode,
WorkflowVariableBlockNode,
VariableValueBlockNode,
VariableValueBlockNode
],
editorState: textToEditorState(value || ''),
onError: (error: Error) => {
throw error
},
}
}
const handleEditorChange = (editorState: EditorState) => {
const text = editorState.read(() => {
return $getRoot().getChildren().map(p => p.getTextContent()).join('\n')
return $getRoot()
.getChildren()
.map((p) => p.getTextContent())
.join('\n')
})
if (onChange)
onChange(text)
if (onChange) onChange(text)
}
useEffect(() => {
eventEmitter?.emit({
type: UPDATE_DATASETS_EVENT_EMITTER,
payload: contextBlock?.datasets,
payload: contextBlock?.datasets
} as any)
}, [eventEmitter, contextBlock?.datasets])
useEffect(() => {
eventEmitter?.emit({
type: UPDATE_HISTORY_EVENT_EMITTER,
payload: historyBlock?.history,
payload: historyBlock?.history
} as any)
}, [eventEmitter, historyBlock?.history])
return (
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
<div className='relative min-h-5'>
<div className="relative min-h-5">
<RichTextPlugin
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-gray-700`} style={style || {}} />}
contentEditable={
<ContentEditable
className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-gray-700`}
style={style || {}}
/>
}
placeholder={<Placeholder value={placeholder} className={placeholderClassName} compact={compact} />}
ErrorBoundary={LexicalErrorBoundary}
/>
<ComponentPickerBlock
triggerString='/'
triggerString="/"
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
@@ -160,7 +147,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
workflowVariableBlock={workflowVariableBlock}
/>
<ComponentPickerBlock
triggerString='{'
triggerString="{"
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
@@ -168,46 +155,36 @@ const PromptEditor: FC<PromptEditorProps> = ({
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
/>
{
contextBlock?.show && (
<>
<ContextBlock {...contextBlock} />
<ContextBlockReplacementBlock {...contextBlock} />
</>
)
}
{
queryBlock?.show && (
<>
<QueryBlock {...queryBlock} />
<QueryBlockReplacementBlock />
</>
)
}
{
historyBlock?.show && (
<>
<HistoryBlock {...historyBlock} />
<HistoryBlockReplacementBlock {...historyBlock} />
</>
)
}
{
(variableBlock?.show || externalToolBlock?.show) && (
<>
<VariableBlock />
<VariableValueBlock />
</>
)
}
{
workflowVariableBlock?.show && (
<>
<WorkflowVariableBlock {...workflowVariableBlock} />
<WorkflowVariableBlockReplacementBlock {...workflowVariableBlock} />
</>
)
}
{contextBlock?.show && (
<>
<ContextBlock {...contextBlock} />
<ContextBlockReplacementBlock {...contextBlock} />
</>
)}
{queryBlock?.show && (
<>
<QueryBlock {...queryBlock} />
<QueryBlockReplacementBlock />
</>
)}
{historyBlock?.show && (
<>
<HistoryBlock {...historyBlock} />
<HistoryBlockReplacementBlock {...historyBlock} />
</>
)}
{(variableBlock?.show || externalToolBlock?.show) && (
<>
<VariableBlock />
<VariableValueBlock />
</>
)}
{workflowVariableBlock?.show && (
<>
<WorkflowVariableBlock {...workflowVariableBlock} />
<WorkflowVariableBlockReplacementBlock {...workflowVariableBlock} />
</>
)}
<OnChangePlugin onChange={handleEditorChange} />
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
<UpdateBlock instanceId={instanceId} />
@@ -1,82 +1,94 @@
import PromptEditor from '@common/components/aoplatform/prompt-editor/PromptEditor.tsx'
import PromptEditorHeightResizeWrap from '@common/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx'
import { useState } from 'react'
import { getVars } from './utils'
import { VariableItems } from '@core/const/ai-service/type'
import PromptEditor from '@common/components/aoplatform/prompt-editor/PromptEditor.tsx';
import PromptEditorHeightResizeWrap from '@common/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx';
import { useEffect, useState } from 'react';
import { getVars } from './utils';
import { VariableItems } from '@core/const/ai-service/type';
const PromptEditorResizable = (props: {
value?: string
onChange?: (value: string) => void
variablesChange?: (keys: string[]) => void
promptVariables: VariableItems[]
disabled?: boolean
}) => {
const { value, onChange, variablesChange, promptVariables, disabled } = props
const minHeight = 68
const [editorHeight, setEditorHeight] = useState(minHeight)
const [previousKeys, setPreviousKeys] = useState<string[]>([])
const handleChange = (newTemplates: string, keys: string[]) => {
onChange?.(newTemplates)
}
const PromptEditorResizable = (props:{value?:string, onChange?:(value:string)=>void, variablesChange?:(keys:string[])=>void,promptVariables:VariableItems[]}) =>{
const {value , onChange,variablesChange,promptVariables} = props
const minHeight = 68
const [editorHeight, setEditorHeight] = useState(minHeight)
const [previousKeys, setPreviousKeys] = useState<string[]>([])
const handleChange = (newTemplates: string, keys: string[]) => {
onChange?.(newTemplates)
return (
<PromptEditorHeightResizeWrap
className="px-4 pt-2 min-h-[94px] bg-white rounded-t-xl text-sm text-gray-700"
height={editorHeight}
minHeight={minHeight}
onHeightChange={setEditorHeight}
hideResize={false}
footer={
<div className="pl-4 pb-2 flex bg-white rounded-b-xl">
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">
{value?.length || 0}
</div>
</div>
}
return ( <PromptEditorHeightResizeWrap
className='px-4 pt-2 min-h-[94px] bg-white rounded-t-xl text-sm text-gray-700'
height={editorHeight}
minHeight={minHeight}
onHeightChange={setEditorHeight}
hideResize={false}
footer={(
<div className='pl-4 pb-2 flex bg-white rounded-b-xl'>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{value?.length || 0}</div>
</div>
>
<>
{value !== undefined && (
<PromptEditor
className="min-h-[68px]"
compact
value={value}
contextBlock={{
show: false,
selectable: true
// datasets: dataSets.map(item => ({
// id: item.id,
// name: item.name,
// type: item.data_source_type,
// })),
// onAddContext: ()=>{console.log('?onAddContext')},
}}
variableBlock={{
show: true,
variables: promptVariables?.map((x) => ({ name: x.key, value: x.key })) || []
}}
externalToolBlock={{
show: false,
externalTools: []
// onAddExternalTool: handleOpenExternalDataToolModal,
}}
historyBlock={{
show: false,
selectable: false,
history: {
user: '',
assistant: ''
},
onEditRole: () => {}
}}
queryBlock={{
show: false,
selectable: true
}}
onChange={(value) => {
handleChange?.(value, [])
}}
onBlur={() => {
const keys = getVars(value)
handleChange(value, keys)
if (keys.filter((key) => !previousKeys.includes(key)).length > 0) {
variablesChange?.(keys)
setPreviousKeys(keys)
}
}}
editable={disabled ? false : true}
/>
)}
><>
{value !== undefined && <PromptEditor
className='min-h-[68px]'
compact
value={value}
contextBlock={{
show: false,
selectable: true,
// datasets: dataSets.map(item => ({
// id: item.id,
// name: item.name,
// type: item.data_source_type,
// })),
// onAddContext: ()=>{console.log('?onAddContext')},
}}
variableBlock={{
show: true,
variables:promptVariables?.map(x=>({name:x.key, value:x.key})) || [],
}}
externalToolBlock={{
show: false,
externalTools: [],
// onAddExternalTool: handleOpenExternalDataToolModal,
}}
historyBlock={{
show: false,
selectable: false,
history: {
user: '',
assistant: '',
},
onEditRole: () => { },
}}
queryBlock={{
show: false,
selectable: true,
}}
onChange={(value) => {
handleChange?.(value, [])
}}
onBlur={() => {
const keys = getVars(value)
handleChange(value, keys)
if(keys.filter(key => !previousKeys.includes(key)).length > 0){
variablesChange?.(keys)
setPreviousKeys(keys)
}
}}
editable={true}
/>
}</>
</PromptEditorHeightResizeWrap>)
</>
</PromptEditorHeightResizeWrap>
)
}
export default PromptEditorResizable
export default PromptEditorResizable
@@ -9,40 +9,35 @@ export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-
export const MAX_VAR_KEY_LENGTH = 30
export const checkHasContextBlock = (text: string) => {
if (!text)
return false
if (!text) return false
return text.includes(CONTEXT_PLACEHOLDER_TEXT)
}
export const checkHasHistoryBlock = (text: string) => {
if (!text)
return false
if (!text) return false
return text.includes(HISTORY_PLACEHOLDER_TEXT)
}
export const checkHasQueryBlock = (text: string) => {
if (!text)
return false
if (!text) return false
return text.includes(QUERY_PLACEHOLDER_TEXT)
}
/*
* {{#1711617514996.name#}} => [1711617514996, name]
* {{#1711617514996.sys.query#}} => [sys, query]
*/
* {{#1711617514996.name#}} => [1711617514996, name]
* {{#1711617514996.sys.query#}} => [sys, query]
*/
export const getInputVars = (text: string) => {
if (!text)
return []
if (!text) return []
const allVars = text.match(/{{#([^#]*)#}}/g)
if (allVars && allVars?.length > 0) {
// {{#context#}}, {{#query#}} is not input vars
const inputVars = allVars
.filter(item => item.includes('.'))
.filter((item) => item.includes('.'))
.map((item) => {
const valueSelector = item.replace('{{#', '').replace('#}}', '').split('.')
if (valueSelector[1] === 'sys' && /^\d+$/.test(valueSelector[0]))
return valueSelector.slice(1)
if (valueSelector[1] === 'sys' && /^\d+$/.test(valueSelector[0])) return valueSelector.slice(1)
return valueSelector
})
@@ -1,16 +1,6 @@
import {
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import type { Dispatch, RefObject, SetStateAction } from 'react'
import type {
Klass,
LexicalCommand,
LexicalEditor,
TextNode,
} from 'lexical'
import type { Klass, LexicalCommand, LexicalEditor, TextNode } from 'lexical'
import {
$getNodeByKey,
$getSelection,
@@ -18,12 +8,10 @@ import {
$isNodeSelection,
COMMAND_PRIORITY_LOW,
KEY_BACKSPACE_COMMAND,
KEY_DELETE_COMMAND,
KEY_DELETE_COMMAND
} from 'lexical'
import type { EntityMatch } from '@lexical/text'
import {
mergeRegister,
} from '@lexical/utils'
import { mergeRegister } from '@lexical/utils'
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $isContextBlockNode } from './plugins/context-block/node'
@@ -35,7 +23,10 @@ import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block'
import type { CustomTextNode } from './plugins/custom-text/node'
import { registerLexicalTextEntity } from './utils'
export type UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement>, boolean]
export type UseSelectOrDeleteHandler = (
nodeKey: string,
command?: LexicalCommand<undefined>
) => [RefObject<HTMLDivElement>, boolean]
export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => {
const ref = useRef<HTMLDivElement>(null)
const [editor] = useLexicalComposerContext()
@@ -46,13 +37,11 @@ export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, com
const selection = $getSelection()
const nodes = selection?.getNodes()
if (
!isSelected
&& nodes?.length === 1
&& (
($isContextBlockNode(nodes[0]) && command === DELETE_CONTEXT_BLOCK_COMMAND)
|| ($isHistoryBlockNode(nodes[0]) && command === DELETE_HISTORY_BLOCK_COMMAND)
|| ($isQueryBlockNode(nodes[0]) && command === DELETE_QUERY_BLOCK_COMMAND)
)
!isSelected &&
nodes?.length === 1 &&
(($isContextBlockNode(nodes[0]) && command === DELETE_CONTEXT_BLOCK_COMMAND) ||
($isHistoryBlockNode(nodes[0]) && command === DELETE_HISTORY_BLOCK_COMMAND) ||
($isQueryBlockNode(nodes[0]) && command === DELETE_QUERY_BLOCK_COMMAND))
)
editor.dispatchCommand(command, undefined)
@@ -60,8 +49,7 @@ export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, com
event.preventDefault()
const node = $getNodeByKey(nodeKey)
if ($isDecoratorNode(node)) {
if (command)
editor.dispatchCommand(command, undefined)
if (command) editor.dispatchCommand(command, undefined)
node.remove()
return true
@@ -70,38 +58,31 @@ export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, com
return false
},
[isSelected, nodeKey, command, editor],
[isSelected, nodeKey, command, editor]
)
const handleSelect = useCallback((e: MouseEvent) => {
e.stopPropagation()
clearSelection()
setSelected(true)
}, [setSelected, clearSelection])
const handleSelect = useCallback(
(e: MouseEvent) => {
e.stopPropagation()
clearSelection()
setSelected(true)
},
[setSelected, clearSelection]
)
useEffect(() => {
const ele = ref.current
if (ele)
ele.addEventListener('click', handleSelect)
if (ele) ele.addEventListener('click', handleSelect)
return () => {
if (ele)
ele.removeEventListener('click', handleSelect)
if (ele) ele.removeEventListener('click', handleSelect)
}
}, [handleSelect])
useEffect(() => {
return mergeRegister(
editor.registerCommand(
KEY_DELETE_COMMAND,
handleDelete,
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
KEY_BACKSPACE_COMMAND,
handleDelete,
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(KEY_DELETE_COMMAND, handleDelete, COMMAND_PRIORITY_LOW),
editor.registerCommand(KEY_BACKSPACE_COMMAND, handleDelete, COMMAND_PRIORITY_LOW)
)
}, [editor, clearSelection, handleDelete])
@@ -114,17 +95,15 @@ export const useTrigger: UseTriggerHandler = () => {
const [open, setOpen] = useState(false)
const handleOpen = useCallback((e: MouseEvent) => {
e.stopPropagation()
setOpen(v => !v)
setOpen((v) => !v)
}, [])
useEffect(() => {
const trigger = triggerRef.current
if (trigger)
trigger.addEventListener('click', handleOpen)
if (trigger) trigger.addEventListener('click', handleOpen)
return () => {
if (trigger)
trigger.removeEventListener('click', handleOpen)
if (trigger) trigger.removeEventListener('click', handleOpen)
}
}, [handleOpen])
@@ -134,7 +113,7 @@ export const useTrigger: UseTriggerHandler = () => {
export function useLexicalTextEntity<T extends TextNode>(
getMatch: (text: string) => null | EntityMatch,
targetNode: Klass<T>,
createNode: (textNode: CustomTextNode) => T,
createNode: (textNode: CustomTextNode) => T
) {
const [editor] = useLexicalComposerContext()
@@ -148,24 +127,16 @@ export type MenuTextMatch = {
matchingString: string
replaceableString: string
}
export type TriggerFn = (
text: string,
editor: LexicalEditor,
) => MenuTextMatch | null
export type TriggerFn = (text: string, editor: LexicalEditor) => MenuTextMatch | null
export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;'
export function useBasicTypeaheadTriggerMatch(
trigger: string,
{ minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number },
{ minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number }
): TriggerFn {
return useCallback(
(text: string) => {
const validChars = `[${PUNCTUATION}\\s]`
const TypeaheadTriggerRegex = new RegExp(
'(.*)('
+ `[${trigger}]`
+ `((?:${validChars}){0,${maxLength}})`
+ ')$',
)
const TypeaheadTriggerRegex = new RegExp('(.*)(' + `[${trigger}]` + `((?:${validChars}){0,${maxLength}})` + ')$')
const match = TypeaheadTriggerRegex.exec(text)
if (match !== null) {
const maybeLeadingWhitespace = match[1]
@@ -174,12 +145,12 @@ export function useBasicTypeaheadTriggerMatch(
return {
leadOffset: match.index + maybeLeadingWhitespace.length,
matchingString,
replaceableString: match[2],
replaceableString: match[2]
}
}
}
return null
},
[maxLength, minLength, trigger],
[maxLength, minLength, trigger]
)
}
@@ -8,7 +8,7 @@ import type {
HistoryBlockType,
QueryBlockType,
VariableBlockType,
WorkflowVariableBlockType,
WorkflowVariableBlockType
} from '../../types'
import { INSERT_CONTEXT_BLOCK_COMMAND } from '../context-block'
import { INSERT_HISTORY_BLOCK_COMMAND } from '../history-block'
@@ -32,32 +32,35 @@ import { $t } from '@common/locales'
export const usePromptOptions = (
contextBlock?: ContextBlockType,
queryBlock?: QueryBlockType,
historyBlock?: HistoryBlockType,
historyBlock?: HistoryBlockType
) => {
const [editor] = useLexicalComposerContext()
const promptOptions: PickerBlockMenuOption[] = []
if (contextBlock?.show) {
promptOptions.push(new PickerBlockMenuOption({
key: $t('上下文'),
group: 'prompt context',
render: ({ isSelected, onSelect, onSetHighlight }) => {
return <PromptMenuItem
title={$t('上下文')}
icon={<></>}
// icon={<File05 className='w-4 h-4 text-[#6938EF]' />}
disabled={!contextBlock.selectable}
isSelected={isSelected}
onClick={onSelect}
onMouseEnter={onSetHighlight}
/>
},
onSelect: () => {
if (!contextBlock?.selectable)
return
editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
},
}))
promptOptions.push(
new PickerBlockMenuOption({
key: $t('上下文'),
group: 'prompt context',
render: ({ isSelected, onSelect, onSetHighlight }) => {
return (
<PromptMenuItem
title={$t('上下文')}
icon={<></>}
// icon={<File05 className='w-4 h-4 text-[#6938EF]' />}
disabled={!contextBlock.selectable}
isSelected={isSelected}
onClick={onSelect}
onMouseEnter={onSetHighlight}
/>
)
},
onSelect: () => {
if (!contextBlock?.selectable) return
editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
}
})
)
}
if (queryBlock?.show) {
@@ -79,11 +82,10 @@ export const usePromptOptions = (
)
},
onSelect: () => {
if (!queryBlock?.selectable)
return
if (!queryBlock?.selectable) return
editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
},
}),
}
})
)
}
@@ -98,8 +100,7 @@ export const usePromptOptions = (
title={$t('会话历史')}
icon={<></>}
// icon={<MessageClockCircle className='w-4 h-4 text-[#DD2590]' />}
disabled={!historyBlock.selectable
}
disabled={!historyBlock.selectable}
isSelected={isSelected}
onClick={onSelect}
onMouseEnter={onSetHighlight}
@@ -107,11 +108,10 @@ export const usePromptOptions = (
)
},
onSelect: () => {
if (!historyBlock?.selectable)
return
if (!historyBlock?.selectable) return
editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
},
}),
}
})
)
}
return promptOptions
@@ -119,16 +119,15 @@ export const usePromptOptions = (
export const useVariableOptions = (
variableBlock?: VariableBlockType,
queryString?: string,
queryString?: string
): PickerBlockMenuOption[] => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const options = useMemo(() => {
if (!variableBlock?.variables)
return []
if (!variableBlock?.variables) return []
const baseOptions = (variableBlock.variables).map((item) => {
const baseOptions = variableBlock.variables.map((item) => {
return new PickerBlockMenuOption({
key: item.value,
group: 'prompt variable',
@@ -147,15 +146,14 @@ export const useVariableOptions = (
},
onSelect: () => {
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`)
},
}
})
})
if (!queryString)
return baseOptions
if (!queryString) return baseOptions
const regex = new RegExp(queryString, 'i')
return baseOptions.filter(option => regex.test(option.key))
return baseOptions.filter((option) => regex.test(option.key))
}, [editor, queryString, variableBlock])
const addOption = useMemo(() => {
@@ -182,7 +180,7 @@ export const useVariableOptions = (
$insertNodes([prefixNode, suffixNode])
prefixNode.select()
})
},
}
})
}, [editor, t])
@@ -191,17 +189,13 @@ export const useVariableOptions = (
}, [options, addOption, variableBlock?.show])
}
export const useExternalToolOptions = (
externalToolBlockType?: ExternalToolBlockType,
queryString?: string,
) => {
export const useExternalToolOptions = (externalToolBlockType?: ExternalToolBlockType, queryString?: string) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const options = useMemo(() => {
if (!externalToolBlockType?.externalTools)
return []
const baseToolOptions = (externalToolBlockType.externalTools).map((item) => {
if (!externalToolBlockType?.externalTools) return []
const baseToolOptions = externalToolBlockType.externalTools.map((item) => {
return new PickerBlockMenuOption({
key: item.name,
group: 'external tool',
@@ -217,7 +211,7 @@ export const useExternalToolOptions = (
// background={item.icon_background}
// />
// }
extraElement={<div className='text-xs text-gray-400'>{item.variableName}</div>}
extraElement={<div className="text-xs text-gray-400">{item.variableName}</div>}
queryString={queryString}
isSelected={isSelected}
onClick={onSelect}
@@ -227,15 +221,14 @@ export const useExternalToolOptions = (
},
onSelect: () => {
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`)
},
}
})
})
if (!queryString)
return baseToolOptions
if (!queryString) return baseToolOptions
const regex = new RegExp(queryString, 'i')
return baseToolOptions.filter(option => regex.test(option.key))
return baseToolOptions.filter((option) => regex.test(option.key))
}, [editor, queryString, externalToolBlockType])
const addOption = useMemo(() => {
@@ -257,7 +250,7 @@ export const useExternalToolOptions = (
},
onSelect: () => {
externalToolBlockType?.onAddExternalTool?.()
},
}
})
}, [externalToolBlockType, t])
@@ -273,14 +266,13 @@ export const useOptions = (
variableBlock?: VariableBlockType,
externalToolBlockType?: ExternalToolBlockType,
workflowVariableBlockType?: WorkflowVariableBlockType,
queryString?: string,
queryString?: string
) => {
const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock)
const variableOptions = useVariableOptions(variableBlock, queryString)
const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString)
const workflowVariableOptions = useMemo(() => {
if (!workflowVariableBlockType?.show)
return []
if (!workflowVariableBlockType?.show) return []
return workflowVariableBlockType.variables || []
}, [workflowVariableBlockType])
@@ -288,7 +280,7 @@ export const useOptions = (
return useMemo(() => {
return {
workflowVariableOptions,
allFlattenOptions: [...promptOptions, ...variableOptions, ...externalToolOptions],
allFlattenOptions: [...promptOptions, ...variableOptions, ...externalToolOptions]
}
}, [promptOptions, variableOptions, externalToolOptions, workflowVariableOptions])
}
@@ -1,16 +1,6 @@
import {
Fragment,
memo,
useCallback,
useState,
} from 'react'
import { Fragment, memo, useCallback, useState } from 'react'
import ReactDOM from 'react-dom'
import {
flip,
offset,
shift,
useFloating,
} from '@floating-ui/react'
import { flip, offset, shift, useFloating } from '@floating-ui/react'
import type { TextNode } from 'lexical'
import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
@@ -21,7 +11,7 @@ import type {
HistoryBlockType,
QueryBlockType,
VariableBlockType,
WorkflowVariableBlockType,
WorkflowVariableBlockType
} from '../../types'
import { useBasicTypeaheadTriggerMatch } from '../../hooks'
import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block'
@@ -48,7 +38,7 @@ const ComponentPicker = ({
historyBlock,
variableBlock,
externalToolBlock,
workflowVariableBlock,
workflowVariableBlock
}: ComponentPickerProps) => {
const { eventEmitter } = useEventEmitterContextContext()
const { refs, floatingStyles, isPositioned } = useFloating({
@@ -56,15 +46,15 @@ const ComponentPicker = ({
middleware: [
offset(0), // fix hide cursor
shift({
padding: 8,
padding: 8
}),
flip(),
],
flip()
]
})
const [editor] = useLexicalComposerContext()
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, {
minLength: 0,
maxLength: 0,
maxLength: 0
})
const [queryString, setQueryString] = useState<string | null>(null)
@@ -74,123 +64,114 @@ const ComponentPicker = ({
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`)
})
const {
allFlattenOptions,
workflowVariableOptions,
} = useOptions(
const { allFlattenOptions, workflowVariableOptions } = useOptions(
contextBlock,
queryBlock,
historyBlock,
variableBlock,
externalToolBlock,
workflowVariableBlock,
workflowVariableBlock
)
const onSelectOption = useCallback(
(
selectedOption: PickerBlockMenuOption,
nodeToRemove: TextNode | null,
closeMenu: () => void,
) => {
(selectedOption: PickerBlockMenuOption, nodeToRemove: TextNode | null, closeMenu: () => void) => {
editor.update(() => {
if (nodeToRemove && selectedOption?.key)
nodeToRemove.remove()
if (nodeToRemove && selectedOption?.key) nodeToRemove.remove()
selectedOption.onSelectMenuOption()
closeMenu()
})
},
[editor],
[editor]
)
const handleSelectWorkflowVariable = useCallback((variables: string[]) => {
editor.update(() => {
const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
if (needRemove)
needRemove.remove()
})
const handleSelectWorkflowVariable = useCallback(
(variables: string[]) => {
editor.update(() => {
const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
if (needRemove) needRemove.remove()
})
if (variables[1] === 'sys.query' || variables[1] === 'sys.files')
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]])
else
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
}, [editor, checkForTriggerMatch, triggerString])
if (variables[1] === 'sys.query' || variables[1] === 'sys.files')
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]])
else editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
},
[editor, checkForTriggerMatch, triggerString]
)
const renderMenu = useCallback<MenuRenderFn<PickerBlockMenuOption>>((
anchorElementRef,
{ options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
) => {
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
return null
refs.setReference(anchorElementRef.current)
const renderMenu = useCallback<MenuRenderFn<PickerBlockMenuOption>>(
(anchorElementRef, { options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => {
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show))) return null
refs.setReference(anchorElementRef.current)
return (
<>
{
ReactDOM.createPortal(
return (
<>
{ReactDOM.createPortal(
// The `LexicalMenu` will try to calculate the position of the floating menu based on the first child.
// Since we use floating ui, we need to wrap it with a div to prevent the position calculation being affected.
// See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493
<div className='w-0 h-0'>
<div className="w-0 h-0">
<div
className='p-1 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg overflow-y-auto overflow-x-hidden'
className="p-1 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg overflow-y-auto overflow-x-hidden"
style={{
...floatingStyles,
visibility: isPositioned ? 'visible' : 'hidden',
maxHeight: 'calc(1 / 3 * 100vh)',
maxHeight: 'calc(1 / 3 * 100vh)'
}}
ref={refs.setFloating}
>
{
options.map((option, index) => (
<Fragment key={option.key}>
{
// Divider
index !== 0 && options.at(index - 1)?.group !== option.group && (
<div className='h-px bg-gray-100 my-1 w-screen -translate-x-1'></div>
)
{options.map((option, index) => (
<Fragment key={option.key}>
{
// Divider
index !== 0 && options.at(index - 1)?.group !== option.group && (
<div className="h-px bg-gray-100 my-1 w-screen -translate-x-1"></div>
)
}
{option.renderMenuOption({
queryString,
isSelected: selectedIndex === index,
onSelect: () => {
selectOptionAndCleanUp(option)
},
onSetHighlight: () => {
setHighlightedIndex(index)
}
{option.renderMenuOption({
queryString,
isSelected: selectedIndex === index,
onSelect: () => {
selectOptionAndCleanUp(option)
},
onSetHighlight: () => {
setHighlightedIndex(index)
},
})}
</Fragment>
))
}
{
workflowVariableBlock?.show && (
<>
{
(!!options.length) && (
<div className='h-px bg-gray-100 my-1 w-screen -translate-x-1'></div>
)
}
<div className='p-1'>
{/* <VarReferenceVars
})}
</Fragment>
))}
{workflowVariableBlock?.show && (
<>
{!!options.length && <div className="h-px bg-gray-100 my-1 w-screen -translate-x-1"></div>}
<div className="p-1">
{/* <VarReferenceVars
hideSearch
vars={workflowVariableOptions}
onChange={(variables: string[]) => {
handleSelectWorkflowVariable(variables)
}}
/> */}
</div>
</>
)
}
</div>
</>
)}
</div>
</div>,
anchorElementRef.current,
)
}
</>
)
}, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable])
anchorElementRef.current
)}
</>
)
},
[
allFlattenOptions.length,
workflowVariableBlock?.show,
refs,
isPositioned,
floatingStyles,
queryString,
workflowVariableOptions,
handleSelectWorkflowVariable
]
)
return (
<LexicalTypeaheadMenuPlugin
@@ -202,7 +183,7 @@ const ComponentPicker = ({
//
// We no need the position function of the `LexicalTypeaheadMenuPlugin`,
// so the reference anchor should be positioned based on the range of the trigger string, and the menu will be positioned by the floating ui.
anchorClassName='z-[999999] translate-y-[calc(-100%-3px)]'
anchorClassName="z-[999999] translate-y-[calc(-100%-3px)]"
menuRenderFn={renderMenu}
triggerFn={checkForTriggerMatch}
/>
@@ -20,12 +20,14 @@ export class PickerBlockMenuOption extends MenuOption {
group?: string
onSelect?: () => void
render: (menuRenderProps: MenuOptionRenderProps) => JSX.Element
},
}
) {
super(data.key)
this.group = data.group
}
public onSelectMenuOption = () => this.data.onSelect?.()
public renderMenuOption = (menuRenderProps: MenuOptionRenderProps) => <Fragment key={this.data.key}>{this.data.render(menuRenderProps)}</Fragment>
public renderMenuOption = (menuRenderProps: MenuOptionRenderProps) => (
<Fragment key={this.data.key}>{this.data.render(menuRenderProps)}</Fragment>
)
}
@@ -9,37 +9,30 @@ type PromptMenuItemMenuItemProps = {
onMouseEnter: () => void
setRefElement?: (element: HTMLDivElement) => void
}
export const PromptMenuItem = memo(({
icon,
title,
disabled,
isSelected,
onClick,
onMouseEnter,
setRefElement,
}: PromptMenuItemMenuItemProps) => {
return (
<div
className={`
export const PromptMenuItem = memo(
({ icon, title, disabled, isSelected, onClick, onMouseEnter, setRefElement }: PromptMenuItemMenuItemProps) => {
return (
<div
className={`
flex items-center px-3 h-6 cursor-pointer hover:bg-gray-50 rounded-md
${isSelected && !disabled && '!bg-gray-50'}
${disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
`}
tabIndex={-1}
ref={setRefElement}
onMouseEnter={() => {
if (disabled)
return
onMouseEnter()
}}
onClick={() => {
if (disabled)
return
onClick()
}}>
{icon}
<div className='ml-1 text-[13px] text-gray-900'>{title}</div>
</div>
)
})
tabIndex={-1}
ref={setRefElement}
onMouseEnter={() => {
if (disabled) return
onMouseEnter()
}}
onClick={() => {
if (disabled) return
onClick()
}}
>
{icon}
<div className="ml-1 text-[13px] text-gray-900">{title}</div>
</div>
)
}
)
PromptMenuItem.displayName = 'PromptMenuItem'
@@ -10,51 +10,52 @@ type VariableMenuItemProps = {
onMouseEnter: () => void
setRefElement?: (element: HTMLDivElement) => void
}
export const VariableMenuItem = memo(({
title,
icon,
extraElement,
isSelected,
queryString,
onClick,
onMouseEnter,
setRefElement,
}: VariableMenuItemProps) => {
let before = title
let middle = ''
let after = ''
export const VariableMenuItem = memo(
({
title,
icon,
extraElement,
isSelected,
queryString,
onClick,
onMouseEnter,
setRefElement
}: VariableMenuItemProps) => {
let before = title
let middle = ''
let after = ''
if (queryString) {
const regex = new RegExp(queryString, 'i')
const match = regex.exec(title)
if (queryString) {
const regex = new RegExp(queryString, 'i')
const match = regex.exec(title)
if (match) {
before = title.substring(0, match.index)
middle = match[0]
after = title.substring(match.index + match[0].length)
if (match) {
before = title.substring(0, match.index)
middle = match[0]
after = title.substring(match.index + match[0].length)
}
}
}
return (
<div
className={`
return (
<div
className={`
flex items-center px-3 h-6 rounded-md hover:bg-[#EBEEF2] cursor-pointer
${isSelected && 'bg-[#EBEEF2]'}
`}
tabIndex={-1}
ref={setRefElement}
onMouseEnter={onMouseEnter}
onClick={onClick}>
<div className='mr-2'>
{icon}
tabIndex={-1}
ref={setRefElement}
onMouseEnter={onMouseEnter}
onClick={onClick}
>
<div className="mr-2">{icon}</div>
<div className="grow text-[13px] text-gray-900 truncate" title={title}>
{before}
<span className="text-[#2970FF]">{middle}</span>
{after}
</div>
{extraElement}
</div>
<div className='grow text-[13px] text-gray-900 truncate' title={title}>
{before}
<span className='text-[#2970FF]'>{middle}</span>
{after}
</div>
{extraElement}
</div>
)
})
)
}
)
VariableMenuItem.displayName = 'VariableMenuItem'
@@ -1,6 +1,5 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
// import {
// RiAddLine,
// } from '@remixicon/react'
@@ -28,7 +27,7 @@ const ContextBlockComponent: FC<ContextBlockComponentProps> = ({
nodeKey,
datasets = [],
onAddContext,
canNotAddContext,
canNotAddContext
}) => {
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CONTEXT_BLOCK_COMMAND)
const [triggerRef, open, setOpen] = useTrigger()
@@ -36,19 +35,20 @@ const ContextBlockComponent: FC<ContextBlockComponentProps> = ({
const [localDatasets, setLocalDatasets] = useState<Dataset[]>(datasets)
eventEmitter?.useSubscription((v: any) => {
if (v?.type === UPDATE_DATASETS_EVENT_EMITTER)
setLocalDatasets(v.payload)
if (v?.type === UPDATE_DATASETS_EVENT_EMITTER) setLocalDatasets(v.payload)
})
return (
<div className={`
<div
className={`
group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent bg-[#F4F3FF] text-[#6938EF] rounded-[5px] hover:bg-[#EBE9FE]
${open ? 'bg-[#EBE9FE]' : 'bg-[#F4F3FF]'}
${isSelected && '!border-[#9B8AFB]'}
`} ref={ref}>
`}
ref={ref}
>
{/* <File05 className='mr-1 w-[14px] h-[14px]' /> */}
<div className='mr-1 text-xs font-medium'>{$t('上下文')}</div>
<div className="mr-1 text-xs font-medium">{$t('上下文')}</div>
</div>
)
}
@@ -1,18 +1,11 @@
import {
memo,
useCallback,
useEffect,
} from 'react'
import { memo, useCallback, useEffect } from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils'
import { CONTEXT_PLACEHOLDER_TEXT } from '../../constants'
import type { ContextBlockType } from '../../types'
import {
$createContextBlockNode,
ContextBlockNode,
} from './node'
import { $createContextBlockNode, ContextBlockNode } from './node'
import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(CONTEXT_PLACEHOLDER_TEXT)
@@ -21,7 +14,7 @@ const ContextBlockReplacementBlock = ({
datasets = [],
onAddContext = () => {},
onInsert,
canNotAddContext,
canNotAddContext
}: ContextBlockType) => {
const [editor] = useLexicalComposerContext()
@@ -31,29 +24,29 @@ const ContextBlockReplacementBlock = ({
}, [editor])
const createContextBlockNode = useCallback((): ContextBlockNode => {
if (onInsert)
onInsert()
if (onInsert) onInsert()
return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext, canNotAddContext))
}, [datasets, onAddContext, onInsert, canNotAddContext])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
if (matchArr === null) return null
const startOffset = matchArr.index
const endOffset = startOffset + CONTEXT_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
start: startOffset
}
}, [])
useEffect(() => {
REGEX.lastIndex = 0
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createContextBlockNode)),
editor.registerNodeTransform(CustomTextNode, (textNode) =>
decoratorTransform(textNode, getMatch, createContextBlockNode)
)
)
}, [])
@@ -1,19 +1,9 @@
import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { memo, useEffect } from 'react'
import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { ContextBlockType } from '../../types'
import {
$createContextBlockNode,
ContextBlockNode,
} from './node'
import { $createContextBlockNode, ContextBlockNode } from './node'
export const INSERT_CONTEXT_BLOCK_COMMAND = createCommand('INSERT_CONTEXT_BLOCK_COMMAND')
export const DELETE_CONTEXT_BLOCK_COMMAND = createCommand('DELETE_CONTEXT_BLOCK_COMMAND')
@@ -24,49 +14,43 @@ export type Dataset = {
type: string
}
const ContextBlock = memo(({
datasets = [],
onAddContext = () => {},
onInsert,
onDelete,
canNotAddContext,
}: ContextBlockType) => {
const [editor] = useLexicalComposerContext()
const ContextBlock = memo(
({ datasets = [], onAddContext = () => {}, onInsert, onDelete, canNotAddContext }: ContextBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([ContextBlockNode]))
throw new Error('ContextBlockPlugin: ContextBlock not registered on editor')
useEffect(() => {
if (!editor.hasNodes([ContextBlockNode]))
throw new Error('ContextBlockPlugin: ContextBlock not registered on editor')
return mergeRegister(
editor.registerCommand(
INSERT_CONTEXT_BLOCK_COMMAND,
() => {
const contextBlockNode = $createContextBlockNode(datasets, onAddContext, canNotAddContext)
return mergeRegister(
editor.registerCommand(
INSERT_CONTEXT_BLOCK_COMMAND,
() => {
const contextBlockNode = $createContextBlockNode(datasets, onAddContext, canNotAddContext)
$insertNodes([contextBlockNode])
$insertNodes([contextBlockNode])
if (onInsert)
onInsert()
if (onInsert) onInsert()
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_CONTEXT_BLOCK_COMMAND,
() => {
if (onDelete)
onDelete()
return true
},
COMMAND_PRIORITY_EDITOR
),
editor.registerCommand(
DELETE_CONTEXT_BLOCK_COMMAND,
() => {
if (onDelete) onDelete()
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, datasets, onAddContext, onInsert, onDelete, canNotAddContext])
return true
},
COMMAND_PRIORITY_EDITOR
)
)
}, [editor, datasets, onAddContext, onInsert, onDelete, canNotAddContext])
return null
})
return null
}
)
ContextBlock.displayName = 'ContextBlock'
export { ContextBlock }
@@ -3,7 +3,11 @@ import { DecoratorNode } from 'lexical'
import ContextBlockComponent from './component'
import type { Dataset } from './index'
export type SerializedNode = SerializedLexicalNode & { datasets: Dataset[]; onAddContext: () => void; canNotAddContext: boolean }
export type SerializedNode = SerializedLexicalNode & {
datasets: Dataset[]
onAddContext: () => void
canNotAddContext: boolean
}
export class ContextBlockNode extends DecoratorNode<JSX.Element> {
__datasets: Dataset[]
@@ -70,7 +74,11 @@ export class ContextBlockNode extends DecoratorNode<JSX.Element> {
}
static importJSON(serializedNode: SerializedNode): ContextBlockNode {
const node = $createContextBlockNode(serializedNode.datasets, serializedNode.onAddContext, serializedNode.canNotAddContext)
const node = $createContextBlockNode(
serializedNode.datasets,
serializedNode.onAddContext,
serializedNode.canNotAddContext
)
return node
}
@@ -81,7 +89,7 @@ export class ContextBlockNode extends DecoratorNode<JSX.Element> {
version: 1,
datasets: this.getDatasets(),
onAddContext: this.getOnAddContext(),
canNotAddContext: this.getCanNotAddContext(),
canNotAddContext: this.getCanNotAddContext()
}
}
@@ -89,12 +97,14 @@ export class ContextBlockNode extends DecoratorNode<JSX.Element> {
return '{{#context#}}'
}
}
export function $createContextBlockNode(datasets: Dataset[], onAddContext: () => void, canNotAddContext?: boolean): ContextBlockNode {
export function $createContextBlockNode(
datasets: Dataset[],
onAddContext: () => void,
canNotAddContext?: boolean
): ContextBlockNode {
return new ContextBlockNode(datasets, onAddContext, undefined, canNotAddContext)
}
export function $isContextBlockNode(
node: ContextBlockNode | LexicalNode | null | undefined,
): boolean {
export function $isContextBlockNode(node: ContextBlockNode | LexicalNode | null | undefined): boolean {
return node instanceof ContextBlockNode
}
@@ -37,13 +37,12 @@ export class CustomTextNode extends TextNode {
style: this.getStyle(),
text: this.getTextContent(),
type: 'custom-text',
version: 1,
version: 1
}
}
isSimpleText() {
return (
(this.__type === 'text' || this.__type === 'custom-text') && this.__mode === 0)
return (this.__type === 'text' || this.__type === 'custom-text') && this.__mode === 0
}
}
@@ -1,6 +1,5 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
// import {
// RiMoreFill,
// } from '@remixicon/react'
@@ -26,7 +25,7 @@ type HistoryBlockComponentProps = {
const HistoryBlockComponent: FC<HistoryBlockComponentProps> = ({
nodeKey,
roleName = { user: '', assistant: '' },
onEditRole,
onEditRole
}) => {
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HISTORY_BLOCK_COMMAND)
const [triggerRef, open, setOpen] = useTrigger()
@@ -34,18 +33,20 @@ const HistoryBlockComponent: FC<HistoryBlockComponentProps> = ({
const [localRoleName, setLocalRoleName] = useState<RoleName>(roleName)
eventEmitter?.useSubscription((v: any) => {
if (v?.type === UPDATE_HISTORY_EVENT_EMITTER)
setLocalRoleName(v.payload)
if (v?.type === UPDATE_HISTORY_EVENT_EMITTER) setLocalRoleName(v.payload)
})
return (
<div className={`
<div
className={`
group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent text-[#DD2590] rounded-[5px] hover:bg-[#FCE7F6]
${open ? 'bg-[#FCE7F6]' : 'bg-[#FDF2FA]'}
${isSelected && '!border-[#F670C7]'}
`} ref={ref}>
`}
ref={ref}
>
{/* <MessageClockCircle className='mr-1 w-[14px] h-[14px]' /> */}
<div className='mr-1 text-xs font-medium'>{$t('会话历史')}</div>
<div className="mr-1 text-xs font-medium">{$t('会话历史')}</div>
</div>
)
}
@@ -1,17 +1,11 @@
import {
useCallback,
useEffect,
} from 'react'
import { useCallback, useEffect } from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils'
import { HISTORY_PLACEHOLDER_TEXT } from '../../constants'
import type { HistoryBlockType } from '../../types'
import {
$createHistoryBlockNode,
HistoryBlockNode,
} from './node'
import { $createHistoryBlockNode, HistoryBlockNode } from './node'
import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT)
@@ -19,7 +13,7 @@ const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT)
const HistoryBlockReplacementBlock = ({
history = { user: '', assistant: '' },
onEditRole = () => {},
onInsert,
onInsert
}: HistoryBlockType) => {
const [editor] = useLexicalComposerContext()
@@ -29,29 +23,29 @@ const HistoryBlockReplacementBlock = ({
}, [editor])
const createHistoryBlockNode = useCallback((): HistoryBlockNode => {
if (onInsert)
onInsert()
if (onInsert) onInsert()
return $applyNodeReplacement($createHistoryBlockNode(history, onEditRole))
}, [history, onEditRole, onInsert])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
if (matchArr === null) return null
const startOffset = matchArr.index
const endOffset = startOffset + HISTORY_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
start: startOffset
}
}, [])
useEffect(() => {
REGEX.lastIndex = 0
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHistoryBlockNode)),
editor.registerNodeTransform(CustomTextNode, (textNode) =>
decoratorTransform(textNode, getMatch, createHistoryBlockNode)
)
)
}, [])
@@ -1,19 +1,9 @@
import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { memo, useEffect } from 'react'
import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { HistoryBlockType } from '../../types'
import {
$createHistoryBlockNode,
HistoryBlockNode,
} from './node'
import { $createHistoryBlockNode, HistoryBlockNode } from './node'
export const INSERT_HISTORY_BLOCK_COMMAND = createCommand('INSERT_HISTORY_BLOCK_COMMAND')
export const DELETE_HISTORY_BLOCK_COMMAND = createCommand('DELETE_HISTORY_BLOCK_COMMAND')
@@ -30,48 +20,43 @@ export type HistoryBlockProps = {
onDelete?: () => void
}
const HistoryBlock = memo(({
history = { user: '', assistant: '' },
onEditRole = () => {},
onInsert,
onDelete,
}: HistoryBlockType) => {
const [editor] = useLexicalComposerContext()
const HistoryBlock = memo(
({ history = { user: '', assistant: '' }, onEditRole = () => {}, onInsert, onDelete }: HistoryBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([HistoryBlockNode]))
throw new Error('HistoryBlockPlugin: HistoryBlock not registered on editor')
useEffect(() => {
if (!editor.hasNodes([HistoryBlockNode]))
throw new Error('HistoryBlockPlugin: HistoryBlock not registered on editor')
return mergeRegister(
editor.registerCommand(
INSERT_HISTORY_BLOCK_COMMAND,
() => {
const historyBlockNode = $createHistoryBlockNode(history, onEditRole)
return mergeRegister(
editor.registerCommand(
INSERT_HISTORY_BLOCK_COMMAND,
() => {
const historyBlockNode = $createHistoryBlockNode(history, onEditRole)
$insertNodes([historyBlockNode])
$insertNodes([historyBlockNode])
if (onInsert)
onInsert()
if (onInsert) onInsert()
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_HISTORY_BLOCK_COMMAND,
() => {
if (onDelete)
onDelete()
return true
},
COMMAND_PRIORITY_EDITOR
),
editor.registerCommand(
DELETE_HISTORY_BLOCK_COMMAND,
() => {
if (onDelete) onDelete()
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, history, onEditRole, onInsert, onDelete])
return true
},
COMMAND_PRIORITY_EDITOR
)
)
}, [editor, history, onEditRole, onInsert, onDelete])
return null
})
return null
}
)
HistoryBlock.displayName = 'HistoryBlock'
export { HistoryBlock }
@@ -40,11 +40,7 @@ export class HistoryBlockNode extends DecoratorNode<JSX.Element> {
decorate(): JSX.Element {
return (
<HistoryBlockComponent
nodeKey={this.getKey()}
roleName={this.getRoleName()}
onEditRole={this.getOnEditRole()}
/>
<HistoryBlockComponent nodeKey={this.getKey()} roleName={this.getRoleName()} onEditRole={this.getOnEditRole()} />
)
}
@@ -71,7 +67,7 @@ export class HistoryBlockNode extends DecoratorNode<JSX.Element> {
type: 'history-block',
version: 1,
roleName: this.getRoleName(),
onEditRole: this.getOnEditRole,
onEditRole: this.getOnEditRole
}
}
@@ -83,8 +79,6 @@ export function $createHistoryBlockNode(roleName: RoleName, onEditRole: () => vo
return new HistoryBlockNode(roleName, onEditRole)
}
export function $isHistoryBlockNode(
node: HistoryBlockNode | LexicalNode | null | undefined,
): node is HistoryBlockNode {
export function $isHistoryBlockNode(node: HistoryBlockNode | LexicalNode | null | undefined): node is HistoryBlockNode {
return node instanceof HistoryBlockNode
}
@@ -1,11 +1,6 @@
import type { FC } from 'react'
import { useEffect, useRef } from 'react'
import {
BLUR_COMMAND,
COMMAND_PRIORITY_EDITOR,
FOCUS_COMMAND,
KEY_ESCAPE_COMMAND,
} from 'lexical'
import { BLUR_COMMAND, COMMAND_PRIORITY_EDITOR, FOCUS_COMMAND, KEY_ESCAPE_COMMAND } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
@@ -14,10 +9,7 @@ type OnBlurBlockProps = {
onBlur?: () => void
onFocus?: () => void
}
const OnBlurBlock: FC<OnBlurBlockProps> = ({
onBlur,
onFocus,
}) => {
const OnBlurBlock: FC<OnBlurBlockProps> = ({ onBlur, onFocus }) => {
const [editor] = useLexicalComposerContext()
const ref = useRef<any>(null)
@@ -33,7 +25,7 @@ const OnBlurBlock: FC<OnBlurBlockProps> = ({
}
return true
},
COMMAND_PRIORITY_EDITOR,
COMMAND_PRIORITY_EDITOR
),
editor.registerCommand(
BLUR_COMMAND,
@@ -42,22 +34,20 @@ const OnBlurBlock: FC<OnBlurBlockProps> = ({
editor.dispatchCommand(KEY_ESCAPE_COMMAND, new KeyboardEvent('keydown', { key: 'Escape' }))
}, 200)
if (onBlur)
onBlur()
if (onBlur) onBlur()
return true
},
COMMAND_PRIORITY_EDITOR,
COMMAND_PRIORITY_EDITOR
),
editor.registerCommand(
FOCUS_COMMAND,
() => {
if (onFocus)
onFocus()
if (onFocus) onFocus()
return true
},
COMMAND_PRIORITY_EDITOR,
),
COMMAND_PRIORITY_EDITOR
)
)
}, [editor, onBlur, onFocus])
@@ -1,18 +1,10 @@
import { $t } from '@common/locales'
import { memo } from 'react'
const Placeholder = ({
compact,
value
}: {
compact?: boolean
value?: string
className?: string
}) => {
const Placeholder = ({ compact, value }: { compact?: boolean; value?: string; className?: string }) => {
return (
<div
className={`absolute top-0 left-0 h-full w-full text-sm text-[#BBB] select-none pointer-events-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'}`}
<div
className={`absolute top-0 left-0 h-full w-full text-sm text-[#BBB] select-none pointer-events-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'}`}
>
{value || $t('AI 模型调用默认仅使用 Query 变量,可输入 “{” 增加新变量。')}
</div>
@@ -1,5 +1,4 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelectOrDelete } from '../../hooks'
import { DELETE_QUERY_BLOCK_COMMAND } from './index'
import { $t } from '@common/locales'
@@ -9,9 +8,7 @@ type QueryBlockComponentProps = {
nodeKey: string
}
const QueryBlockComponent: FC<QueryBlockComponentProps> = ({
nodeKey,
}) => {
const QueryBlockComponent: FC<QueryBlockComponentProps> = ({ nodeKey }) => {
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_QUERY_BLOCK_COMMAND)
return (
@@ -23,9 +20,9 @@ const QueryBlockComponent: FC<QueryBlockComponentProps> = ({
ref={ref}
>
{/* <UserEdit02 className='mr-1 w-[14px] h-[14px] text-[#FD853A]' /> */}
<div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'{{'}</div>
<div className='text-xs font-medium text-[#EC4A0A]'>{$t('查询内容')}</div>
<div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'}}'}</div>
<div className="text-xs font-medium text-[#EC4A0A] opacity-60">{'{{'}</div>
<div className="text-xs font-medium text-[#EC4A0A]">{$t('查询内容')}</div>
<div className="text-xs font-medium text-[#EC4A0A] opacity-60">{'}}'}</div>
</div>
)
}
@@ -1,19 +1,9 @@
import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { memo, useEffect } from 'react'
import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { QueryBlockType } from '../../types'
import {
$createQueryBlockNode,
QueryBlockNode,
} from './node'
import { $createQueryBlockNode, QueryBlockNode } from './node'
export const INSERT_QUERY_BLOCK_COMMAND = createCommand('INSERT_QUERY_BLOCK_COMMAND')
export const DELETE_QUERY_BLOCK_COMMAND = createCommand('DELETE_QUERY_BLOCK_COMMAND')
@@ -22,15 +12,11 @@ export type QueryBlockProps = {
onInsert?: () => void
onDelete?: () => void
}
const QueryBlock = memo(({
onInsert,
onDelete,
}: QueryBlockType) => {
const QueryBlock = memo(({ onInsert, onDelete }: QueryBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([QueryBlockNode]))
throw new Error('QueryBlockPlugin: QueryBlock not registered on editor')
if (!editor.hasNodes([QueryBlockNode])) throw new Error('QueryBlockPlugin: QueryBlock not registered on editor')
return mergeRegister(
editor.registerCommand(
@@ -39,23 +25,21 @@ const QueryBlock = memo(({
const contextBlockNode = $createQueryBlockNode()
$insertNodes([contextBlockNode])
if (onInsert)
onInsert()
if (onInsert) onInsert()
return true
},
COMMAND_PRIORITY_EDITOR,
COMMAND_PRIORITY_EDITOR
),
editor.registerCommand(
DELETE_QUERY_BLOCK_COMMAND,
() => {
if (onDelete)
onDelete()
if (onDelete) onDelete()
return true
},
COMMAND_PRIORITY_EDITOR,
),
COMMAND_PRIORITY_EDITOR
)
)
}, [editor, onInsert, onDelete])
@@ -40,7 +40,7 @@ export class QueryBlockNode extends DecoratorNode<JSX.Element> {
exportJSON(): SerializedNode {
return {
type: 'query-block',
version: 1,
version: 1
}
}
@@ -52,8 +52,6 @@ export function $createQueryBlockNode(): QueryBlockNode {
return new QueryBlockNode()
}
export function $isQueryBlockNode(
node: QueryBlockNode | LexicalNode | null | undefined,
): node is QueryBlockNode {
export function $isQueryBlockNode(node: QueryBlockNode | LexicalNode | null | undefined): node is QueryBlockNode {
return node instanceof QueryBlockNode
}
@@ -1,25 +1,16 @@
import {
memo,
useCallback,
useEffect,
} from 'react'
import { memo, useCallback, useEffect } from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils'
import { QUERY_PLACEHOLDER_TEXT } from '../../constants'
import type { QueryBlockType } from '../../types'
import {
$createQueryBlockNode,
QueryBlockNode,
} from './node'
import { $createQueryBlockNode, QueryBlockNode } from './node'
import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(QUERY_PLACEHOLDER_TEXT)
const QueryBlockReplacementBlock = ({
onInsert,
}: QueryBlockType) => {
const QueryBlockReplacementBlock = ({ onInsert }: QueryBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
@@ -28,29 +19,29 @@ const QueryBlockReplacementBlock = ({
}, [editor])
const createQueryBlockNode = useCallback((): QueryBlockNode => {
if (onInsert)
onInsert()
if (onInsert) onInsert()
return $applyNodeReplacement($createQueryBlockNode())
}, [onInsert])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
if (matchArr === null) return null
const startOffset = matchArr.index
const endOffset = startOffset + QUERY_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
start: startOffset
}
}, [])
useEffect(() => {
REGEX.lastIndex = 0
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createQueryBlockNode)),
editor.registerNodeTransform(CustomTextNode, (textNode) =>
decoratorTransform(textNode, getMatch, createQueryBlockNode)
)
)
}, [])
@@ -11,9 +11,7 @@ export const PROMPT_EDITOR_INSERT_QUICKLY = 'PROMPT_EDITOR_INSERT_QUICKLY'
type UpdateBlockProps = {
instanceId?: string
}
const UpdateBlock = ({
instanceId,
}: UpdateBlockProps) => {
const UpdateBlock = ({ instanceId }: UpdateBlockProps) => {
const { eventEmitter } = useEventEmitterContextContext()
const [editor] = useLexicalComposerContext()
@@ -1,9 +1,5 @@
import { useEffect } from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { CustomTextNode } from '../custom-text/node'
@@ -24,7 +20,7 @@ const VariableBlock = () => {
return true
},
COMMAND_PRIORITY_EDITOR,
COMMAND_PRIORITY_EDITOR
),
editor.registerCommand(
INSERT_VARIABLE_VALUE_BLOCK_COMMAND,
@@ -34,8 +30,8 @@ const VariableBlock = () => {
return true
},
COMMAND_PRIORITY_EDITOR,
),
COMMAND_PRIORITY_EDITOR
)
)
}, [editor])
@@ -1,14 +1,8 @@
import {
useCallback,
useEffect,
} from 'react'
import { useCallback, useEffect } from 'react'
import type { TextNode } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useLexicalTextEntity } from '../../hooks'
import {
$createVariableValueBlockNode,
VariableValueBlockNode,
} from './node'
import { $createVariableValueBlockNode, VariableValueBlockNode } from './node'
import { getHashtagRegexString } from './utils'
const REGEX = new RegExp(getHashtagRegexString(), 'i')
@@ -28,22 +22,21 @@ const VariableValueBlock = () => {
const getVariableValueMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
if (matchArr === null) return null
const hashtagLength = matchArr[0].length
const startOffset = matchArr.index
const endOffset = startOffset + hashtagLength
return {
end: endOffset,
start: startOffset,
start: startOffset
}
}, [])
useLexicalTextEntity<VariableValueBlockNode>(
getVariableValueMatch,
VariableValueBlockNode,
createVariableValueBlockNode,
createVariableValueBlockNode
)
return null
@@ -1,13 +1,5 @@
import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedTextNode,
} from 'lexical'
import {
$applyNodeReplacement,
TextNode,
} from 'lexical'
import type { EditorConfig, LexicalNode, NodeKey, SerializedTextNode } from 'lexical'
import { $applyNodeReplacement, TextNode } from 'lexical'
export class VariableValueBlockNode extends TextNode {
static getType(): string {
@@ -24,7 +16,15 @@ export class VariableValueBlockNode extends TextNode {
createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config)
element.classList.add('inline-flex', 'items-center', 'px-0.5', 'h-[22px]', 'text-[#155EEF]', 'rounded-[5px]', 'align-middle')
element.classList.add(
'inline-flex',
'items-center',
'px-0.5',
'h-[22px]',
'text-[#155EEF]',
'rounded-[5px]',
'align-middle'
)
return element
}
@@ -45,7 +45,7 @@ export class VariableValueBlockNode extends TextNode {
style: this.getStyle(),
text: this.getTextContent(),
type: 'variable-value-block',
version: 1,
version: 1
}
}
@@ -58,8 +58,6 @@ export function $createVariableValueBlockNode(text = ''): VariableValueBlockNode
return $applyNodeReplacement(new VariableValueBlockNode(text))
}
export function $isVariableValueNodeBlock(
node: LexicalNode | null | undefined,
): node is VariableValueBlockNode {
export function $isVariableValueNodeBlock(node: LexicalNode | null | undefined): node is VariableValueBlockNode {
return node instanceof VariableValueBlockNode
}
@@ -1,12 +1,6 @@
import {
memo,
useEffect,
useState,
} from 'react'
import { memo, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
COMMAND_PRIORITY_EDITOR,
} from 'lexical'
import { COMMAND_PRIORITY_EDITOR } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
// import {
@@ -15,10 +9,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { useSelectOrDelete } from '../../hooks'
import type { WorkflowNodesMap } from './node'
import { WorkflowVariableBlockNode } from './node'
import {
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
UPDATE_WORKFLOW_NODES_MAP,
} from './index'
import { DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND, UPDATE_WORKFLOW_NODES_MAP } from './index'
// import cn from '@/utils/classnames'
// import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
// import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
@@ -36,7 +27,7 @@ type WorkflowVariableBlockComponentProps = {
const WorkflowVariableBlockComponent = ({
nodeKey,
variables,
workflowNodesMap = {},
workflowNodesMap = {}
}: WorkflowVariableBlockComponentProps) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
@@ -66,14 +57,14 @@ const WorkflowVariableBlockComponent = ({
return true
},
COMMAND_PRIORITY_EDITOR,
),
COMMAND_PRIORITY_EDITOR
)
)
}, [editor])
const Item = (
<div
className={`mx-0.5 relative group/wrap flex items-center h-[18px] pl-0.5 pr-[3px] rounded-[5px] border select-none ${isSelected ? 'border-[#84ADFF] bg-[#F5F8FF]' : 'border-black/5 bg-white'}` }
className={`mx-0.5 relative group/wrap flex items-center h-[18px] pl-0.5 pr-[3px] rounded-[5px] border select-none ${isSelected ? 'border-[#84ADFF] bg-[#F5F8FF]' : 'border-black/5 bg-white'}`}
ref={ref}
>
{/* {!isEnv && !isChatVar && (
@@ -93,7 +84,7 @@ const WorkflowVariableBlockComponent = ({
<Line3 className='mr-0.5 text-gray-300'></Line3>
</div>
)} */}
<div className='flex items-center text-primary-600'>
<div className="flex items-center text-primary-600">
{/* {!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
@@ -107,7 +98,6 @@ const WorkflowVariableBlockComponent = ({
</div>
)
return Item
}
@@ -1,19 +1,9 @@
import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { memo, useEffect } from 'react'
import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { WorkflowVariableBlockType } from '../../types'
import {
$createWorkflowVariableBlockNode,
WorkflowVariableBlockNode,
} from './node'
import { $createWorkflowVariableBlockNode, WorkflowVariableBlockNode } from './node'
// import type { Node } from '@/app/components/workflow/types'
export const INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND')
@@ -26,11 +16,7 @@ export type WorkflowVariableBlockProps = {
onInsert?: () => void
onDelete?: () => void
}
const WorkflowVariableBlock = memo(({
workflowNodesMap,
onInsert,
onDelete,
}: WorkflowVariableBlockType) => {
const WorkflowVariableBlock = memo(({ workflowNodesMap, onInsert, onDelete }: WorkflowVariableBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
@@ -51,23 +37,21 @@ const WorkflowVariableBlock = memo(({
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap)
$insertNodes([workflowVariableBlockNode])
if (onInsert)
onInsert()
if (onInsert) onInsert()
return true
},
COMMAND_PRIORITY_EDITOR,
COMMAND_PRIORITY_EDITOR
),
editor.registerCommand(
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
() => {
if (onDelete)
onDelete()
if (onDelete) onDelete()
return true
},
COMMAND_PRIORITY_EDITOR,
),
COMMAND_PRIORITY_EDITOR
)
)
}, [editor, onInsert, onDelete, workflowNodesMap])
@@ -63,7 +63,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
type: 'workflow-variable-block',
version: 1,
variables: this.getVariables(),
workflowNodesMap: this.getWorkflowNodesMap(),
workflowNodesMap: this.getWorkflowNodesMap()
}
}
@@ -81,12 +81,15 @@ export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
return `{{#${this.getVariables().join('.')}#}}`
}
}
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap): WorkflowVariableBlockNode {
export function $createWorkflowVariableBlockNode(
variables: string[],
workflowNodesMap: WorkflowNodesMap
): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(variables, workflowNodesMap)
}
export function $isWorkflowVariableBlockNode(
node: WorkflowVariableBlockNode | LexicalNode | null | undefined,
node: WorkflowVariableBlockNode | LexicalNode | null | undefined
): node is WorkflowVariableBlockNode {
return node instanceof WorkflowVariableBlockNode
}
@@ -1,8 +1,4 @@
import {
memo,
useCallback,
useEffect,
} from 'react'
import { memo, useCallback, useEffect } from 'react'
import type { TextNode } from 'lexical'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
@@ -16,10 +12,7 @@ import { WorkflowVariableBlockNode } from './index'
export const REGEX = /\{\{(#[a-zA-Z0-9_-]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}/gi
const WorkflowVariableBlockReplacementBlock = ({
workflowNodesMap,
onInsert,
}: WorkflowVariableBlockType) => {
const WorkflowVariableBlockReplacementBlock = ({ workflowNodesMap, onInsert }: WorkflowVariableBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
@@ -27,37 +20,39 @@ const WorkflowVariableBlockReplacementBlock = ({
throw new Error('WorkflowVariableBlockNodePlugin: WorkflowVariableBlockNode not registered on editor')
}, [editor])
const createWorkflowVariableBlockNode = useCallback((textNode: TextNode): WorkflowVariableBlockNode => {
if (onInsert)
onInsert()
const createWorkflowVariableBlockNode = useCallback(
(textNode: TextNode): WorkflowVariableBlockNode => {
if (onInsert) onInsert()
const nodePathString = textNode.getTextContent().slice(3, -3)
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap))
}, [onInsert, workflowNodesMap])
const nodePathString = textNode.getTextContent().slice(3, -3)
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap))
},
[onInsert, workflowNodesMap]
)
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
if (matchArr === null) return null
const startOffset = matchArr.index
const endOffset = startOffset + matchArr[0].length
return {
end: endOffset,
start: startOffset,
start: startOffset
}
}, [])
const transformListener = useCallback((textNode: any) => {
return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode)
}, [createWorkflowVariableBlockNode, getMatch])
const transformListener = useCallback(
(textNode: any) => {
return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode)
},
[createWorkflowVariableBlockNode, getMatch]
)
useEffect(() => {
REGEX.lastIndex = 0
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, transformListener),
)
return mergeRegister(editor.registerNodeTransform(CustomTextNode, transformListener))
}, [])
return null
@@ -20,7 +20,7 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
onHeightChange,
children,
footer,
hideResize,
hideResize
}) => {
const [clientY, setClientY] = useState(0)
const [isResizing, setIsResizing] = useState(false)
@@ -38,19 +38,20 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
document.body.style.userSelect = prevUserSelectStyle
}, [prevUserSelectStyle])
const { run: didHandleResize } = useDebounceFn((e) => {
if (!isResizing)
return
const { run: didHandleResize } = useDebounceFn(
(e) => {
if (!isResizing) return
const offset = e.clientY - clientY
let newHeight = height + offset
setClientY(e.clientY)
if (newHeight < minHeight)
newHeight = minHeight
onHeightChange(newHeight)
}, {
wait: 0,
})
const offset = e.clientY - clientY
let newHeight = height + offset
setClientY(e.clientY)
if (newHeight < minHeight) newHeight = minHeight
onHeightChange(newHeight)
},
{
wait: 0
}
)
const handleResize = useCallback(didHandleResize, [isResizing, height, minHeight, clientY])
@@ -69,12 +70,11 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
}, [handleStopResize])
return (
<div
className='relative rounded ant-input-outlined'
>
<div className={`${className} overflow-y-auto`}
<div className="relative rounded ant-input-outlined">
<div
className={`${className} overflow-y-auto`}
style={{
height,
height
}}
>
{children}
@@ -83,9 +83,10 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
{footer}
{!hideResize && (
<div
className='absolute bottom-0 left-0 w-full flex justify-center h-2 cursor-row-resize'
onMouseDown={handleStartResize}>
<div className='w-5 h-[3px] rounded-sm bg-gray-300'></div>
className="absolute bottom-0 left-0 w-full flex justify-center h-2 cursor-row-resize"
onMouseDown={handleStartResize}
>
<div className="w-5 h-[3px] rounded-sm bg-gray-300"></div>
</div>
)}
</div>
@@ -4,7 +4,6 @@ import type { RoleName } from './plugins/history-block/index'
// Node,
// } from '@/app/components/workflow/types'
export type NodeOutPutVar = {
nodeId: string
title: string
@@ -12,7 +11,6 @@ export type NodeOutPutVar = {
isStartNode?: boolean
}
export type Option = {
value: string
name: string
@@ -1,45 +1,34 @@
import { $isAtNodeEnd } from '@lexical/selection'
import type {
ElementNode,
Klass,
LexicalEditor,
LexicalNode,
RangeSelection,
TextNode,
} from 'lexical'
import {
$createTextNode,
$getSelection,
$isRangeSelection,
$isTextNode,
} from 'lexical'
import type { ElementNode, Klass, LexicalEditor, LexicalNode, RangeSelection, TextNode } from 'lexical'
import { $createTextNode, $getSelection, $isRangeSelection, $isTextNode } from 'lexical'
import type { EntityMatch } from '@lexical/text'
import { CustomTextNode } from './plugins/custom-text/node'
import type { MenuTextMatch } from './types'
import { CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT, MAX_VAR_KEY_LENGTH } from './constants'
import {
CONTEXT_PLACEHOLDER_TEXT,
HISTORY_PLACEHOLDER_TEXT,
QUERY_PLACEHOLDER_TEXT,
PRE_PROMPT_PLACEHOLDER_TEXT,
MAX_VAR_KEY_LENGTH
} from './constants'
export function getSelectedNode(
selection: RangeSelection,
): TextNode | ElementNode {
export function getSelectedNode(selection: RangeSelection): TextNode | ElementNode {
const anchor = selection.anchor
const focus = selection.focus
const anchorNode = selection.anchor.getNode()
const focusNode = selection.focus.getNode()
if (anchorNode === focusNode)
return anchorNode
if (anchorNode === focusNode) return anchorNode
const isBackward = selection.isBackward()
if (isBackward)
return $isAtNodeEnd(focus) ? anchorNode : focusNode
else
return $isAtNodeEnd(anchor) ? anchorNode : focusNode
if (isBackward) return $isAtNodeEnd(focus) ? anchorNode : focusNode
else return $isAtNodeEnd(anchor) ? anchorNode : focusNode
}
export function registerLexicalTextEntity<T extends TextNode>(
editor: LexicalEditor,
getMatch: (text: string) => null | EntityMatch,
targetNode: Klass<T>,
createNode: (textNode: TextNode) => T,
createNode: (textNode: TextNode) => T
) {
const isTargetNode = (node: LexicalNode | null | undefined): node is T => {
return node instanceof targetNode
@@ -56,8 +45,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
}
const textNodeTransform = (node: TextNode) => {
if (!node.isSimpleText())
return
if (!node.isSimpleText()) return
const prevSibling = node.getPreviousSibling()
let text = node.getTextContent()
@@ -73,8 +61,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
if (prevMatch === null || getMode(prevSibling) !== 0) {
replaceWithSimpleText(prevSibling)
return
}
else {
} else {
const diff = prevMatch.end - previousText.length
if (diff > 0) {
@@ -85,8 +72,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
if (diff === text.length) {
node.remove()
}
else {
} else {
const remainingText = text.slice(diff)
node.setTextContent(remainingText)
}
@@ -94,8 +80,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
return
}
}
}
else if (prevMatch === null || prevMatch.start < previousText.length) {
} else if (prevMatch === null || prevMatch.start < previousText.length) {
return
}
}
@@ -113,44 +98,34 @@ export function registerLexicalTextEntity<T extends TextNode>(
const nextMatch = getMatch(nextText)
if (nextMatch === null) {
if (isTargetNode(nextSibling))
replaceWithSimpleText(nextSibling)
else
nextSibling.markDirty()
if (isTargetNode(nextSibling)) replaceWithSimpleText(nextSibling)
else nextSibling.markDirty()
return
}
else if (nextMatch.start !== 0) {
} else if (nextMatch.start !== 0) {
return
}
}
}
else {
} else {
const nextMatch = getMatch(nextText)
if (nextMatch !== null && nextMatch.start === 0)
return
if (nextMatch !== null && nextMatch.start === 0) return
}
if (match === null)
return
if (match === null) return
if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity())
continue
if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity()) continue
let nodeToReplace
if (match.start === 0)
[nodeToReplace, currentNode] = currentNode.splitText(match.end)
else
[, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
if (match.start === 0) [nodeToReplace, currentNode] = currentNode.splitText(match.end)
else [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
const replacementNode = createNode(nodeToReplace)
replacementNode.setFormat(nodeToReplace.getFormat())
nodeToReplace.replace(replacementNode)
if (currentNode == null)
return
if (currentNode == null) return
}
}
@@ -181,8 +156,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) {
replaceWithSimpleText(nextSibling) // This may have already been converted in the previous block
if (isTargetNode(node))
replaceWithSimpleText(node)
if (isTargetNode(node)) replaceWithSimpleText(node)
}
}
@@ -194,10 +168,9 @@ export function registerLexicalTextEntity<T extends TextNode>(
export const decoratorTransform = (
node: CustomTextNode,
getMatch: (text: string) => null | EntityMatch,
createNode: (textNode: TextNode) => LexicalNode,
createNode: (textNode: TextNode) => LexicalNode
) => {
if (!node.isSimpleText())
return
if (!node.isSimpleText()) return
const prevSibling = node.getPreviousSibling()
let text = node.getTextContent()
@@ -219,79 +192,56 @@ export const decoratorTransform = (
if (nextMatch === null) {
nextSibling.markDirty()
return
}
else if (nextMatch.start !== 0) {
} else if (nextMatch.start !== 0) {
return
}
}
}
else {
} else {
const nextMatch = getMatch(nextText)
if (nextMatch !== null && nextMatch.start === 0)
return
if (nextMatch !== null && nextMatch.start === 0) return
}
if (match === null)
return
if (match === null) return
if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity())
continue
if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity()) continue
let nodeToReplace
if (match.start === 0)
[nodeToReplace, currentNode] = currentNode.splitText(match.end)
else
[, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
if (match.start === 0) [nodeToReplace, currentNode] = currentNode.splitText(match.end)
else [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
const replacementNode = createNode(nodeToReplace)
nodeToReplace.replace(replacementNode)
if (currentNode == null)
return
if (currentNode == null) return
}
}
function getFullMatchOffset(
documentText: string,
entryText: string,
offset: number,
): number {
function getFullMatchOffset(documentText: string, entryText: string, offset: number): number {
let triggerOffset = offset
for (let i = triggerOffset; i <= entryText.length; i++) {
if (documentText.substr(-i) === entryText.substr(0, i))
triggerOffset = i
if (documentText.substr(-i) === entryText.substr(0, i)) triggerOffset = i
}
return triggerOffset
}
export function $splitNodeContainingQuery(match: MenuTextMatch): TextNode | null {
const selection = $getSelection()
if (!$isRangeSelection(selection) || !selection.isCollapsed())
return null
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return null
const anchor = selection.anchor
if (anchor.type !== 'text')
return null
if (anchor.type !== 'text') return null
const anchorNode = anchor.getNode()
if (!anchorNode.isSimpleText())
return null
if (!anchorNode.isSimpleText()) return null
const selectionOffset = anchor.offset
const textContent = anchorNode.getTextContent().slice(0, selectionOffset)
const characterOffset = match.replaceableString.length
const queryOffset = getFullMatchOffset(
textContent,
match.matchingString,
characterOffset,
)
const queryOffset = getFullMatchOffset(textContent, match.matchingString, characterOffset)
const startOffset = selectionOffset - queryOffset
if (startOffset < 0)
return null
if (startOffset < 0) return null
let newNode
if (startOffset === 0)
[newNode] = anchorNode.splitText(selectionOffset)
else
[, newNode] = anchorNode.splitText(startOffset, selectionOffset)
if (startOffset === 0) [newNode] = anchorNode.splitText(selectionOffset)
else [, newNode] = anchorNode.splitText(startOffset, selectionOffset)
return newNode
}
@@ -303,49 +253,57 @@ export function textToEditorState(text: string) {
root: {
children: paragraph.map((p) => {
return {
children: [{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: p,
type: 'custom-text',
version: 1,
}],
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: p,
type: 'custom-text',
version: 1
}
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
version: 1
}
}),
direction: 'ltr',
format: '',
indent: 0,
type: 'root',
version: 1,
},
version: 1
}
})
}
const varRegex = /\{\{(.+?)\}\}/g
export const getVars = (value: string) => {
if (!value)
return []
if (!value) return []
const keys = value.match(varRegex)?.filter((item) => {
return ![CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT].includes(item)
}).map((item) => {
return item.replace('{{', '').replace('}}', '')
}).filter(key => key.length <= MAX_VAR_KEY_LENGTH) || []
const keys =
value
.match(varRegex)
?.filter((item) => {
return ![
CONTEXT_PLACEHOLDER_TEXT,
HISTORY_PLACEHOLDER_TEXT,
QUERY_PLACEHOLDER_TEXT,
PRE_PROMPT_PLACEHOLDER_TEXT
].includes(item)
})
.map((item) => {
return item.replace('{{', '').replace('}}', '')
})
.filter((key) => key.length <= MAX_VAR_KEY_LENGTH) || []
const keyObj: Record<string, boolean> = {}
// remove duplicate keys
const res: string[] = []
keys.forEach((key) => {
if (keyObj[key])
return
if (keyObj[key]) return
keyObj[key] = true
res.push(key)
@@ -1,14 +1,6 @@
import { $t } from "@common/locales"
import { $t } from '@common/locales'
export type PARAM_TYPE =
| 'string'
| 'float'
| 'integer'
| 'boolean'
| 'date'
| 'time'
| 'datatime'
| string
export type PARAM_TYPE = 'string' | 'float' | 'integer' | 'boolean' | 'date' | 'time' | 'datatime' | string
export type PARAM_KEY_REF_TYPE = {
key: string
type: string
@@ -17,7 +9,7 @@ export type PARAM_KEY_REF_TYPE = {
attribute?: string
description?: string
filter?: string
arrayItemKey?:string
arrayItemKey?: string
}
export type PARAM_TYPE_REF_TYPE = {
[key: string | number]: PARAM_TYPE
@@ -1,8 +1,17 @@
import { cloneDeep } from 'lodash-es'
import { parseFormData, parseFileValue, parseFileType, parseHeaders, parseRequestBodyToString, parseUri, payloadStr, goCodeParseFormData } from './transform'
import {
parseFormData,
parseFileValue,
parseFileType,
parseHeaders,
parseRequestBodyToString,
parseUri,
payloadStr,
goCodeParseFormData
} from './transform'
// import { getJson } from '../.@common/utils/';
import { ApiBodyType } from '@common/const/api-detail';
import { $t } from '@common/locales';
import { ApiBodyType } from '@common/const/api-detail'
import { $t } from '@common/locales'
function sameNameToParams(params: unknown) {
params = cloneDeep(params)
@@ -54,9 +63,9 @@ function enrichParams(params: unknown) {
export function generateCode(
type: string,
multipart: boolean,
{ protocol, URL, headers, params, method, requestType, apiRequestParamJsonType, raw }: unknown,
{ protocol, URL, headers, params, method, requestType, apiRequestParamJsonType, raw }: unknown
) {
requestType=ApiBodyType[requestType]
requestType = ApiBodyType[requestType]
let code: string = ''
const indent = ' '
let urlObj: unknown = {}
@@ -243,9 +252,7 @@ export function generateCode(
`${indent}"headers": {\r\n` +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
`${indent}},\r\n` +
`${
multipart ? `${indent}"processData": false,\r\n${indent}"contentType": false,\r\n` : ''
}` +
`${multipart ? `${indent}"processData": false,\r\n${indent}"contentType": false,\r\n` : ''}` +
`${indent}"data": data,\r\n` +
`${indent}"crossDomain": true\r\n` +
'})\r\n' +
@@ -334,14 +341,10 @@ export function generateCode(
'var requestInfo={\r\n' +
`${indent}"method": "${method}",\r\n` +
`${urlObj.hostname ? `${indent}"hostname": "${urlObj.hostname}",\r\n` : ''}` +
`${
urlObj.port ? `${indent}"port": "${urlObj.port}",\r\n` : ''
}` +
`${urlObj.port ? `${indent}"port": "${urlObj.port}",\r\n` : ''}` +
`${
urlObj.pathname || urlObj.search
? `${indent}"path": "${urlObj.pathname || ''}${
urlObj.search || ''
}",\r\n`
? `${indent}"path": "${urlObj.pathname || ''}${urlObj.search || ''}",\r\n`
: ''
}` +
`${indent}"headers": {\r\n` +
@@ -366,19 +369,11 @@ export function generateCode(
`var http = require("${urlObj.protocol.replace(':', '')}");\r\n` +
'var requestInfo={\r\n' +
`${indent}"method": "${method}",\r\n` +
`${
urlObj.hostname
? `${indent}"hostname": "${urlObj.hostname}",\r\n`
: ''
}` +
`${
urlObj.port ? `${indent}"port": "${urlObj.port}",\r\n` : ''
}` +
`${urlObj.hostname ? `${indent}"hostname": "${urlObj.hostname}",\r\n` : ''}` +
`${urlObj.port ? `${indent}"port": "${urlObj.port}",\r\n` : ''}` +
`${
urlObj.pathname || urlObj.search
? `${indent}"path": "${urlObj.pathname || ''}${
urlObj.search || ''
}",\r\n`
? `${indent}"path": "${urlObj.pathname || ''}${urlObj.search || ''}",\r\n`
: ''
}` +
`${indent}"headers": {\r\n` +
@@ -439,11 +434,7 @@ export function generateCode(
}
}
code =
`${
(requestType || 'FORMDATA') === 'FORMDATA' && multipart
? 'var fs = require("fs");\r\n'
: ''
}` +
`${(requestType || 'FORMDATA') === 'FORMDATA' && multipart ? 'var fs = require("fs");\r\n' : ''}` +
'var request = require("request");\r\n' +
'var requestInfo={\r\n' +
` method: "${method}",\r\n` +
@@ -472,7 +463,7 @@ export function generateCode(
params = sameNameToParams(params)
let tmpOutput: unknown = ''
params.map((val: unknown, key: number) => {
if(val.data_type === 'file') {
if (val.data_type === 'file') {
tmpOutput +=
` "${val.name}" =>array(\r\n` +
` "type" => "${parseFileType(val.value)}",\r\n` +
@@ -481,11 +472,9 @@ export function generateCode(
` )${key === params.length - 1 ? '' : ','}\r\n`
} else {
tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r`
}
})
langTmp.paramsStr =
'addForm(' + `${tmpOutput.length ? `array(\r\n${tmpOutput})` : ''}` + '\r\n);'
langTmp.paramsStr = 'addForm(' + `${tmpOutput.length ? `array(\r\n${tmpOutput})` : ''}` + '\r\n);'
langTmp.fileValue = parseFileValue(params)
} else {
langTmp.paramsStr = `append(new http\\QueryString(array({\r\n${parseFormData(params, {
@@ -516,9 +505,7 @@ export function generateCode(
const tmpOutput: unknown = []
urlObj.searchParams.forEach((val: unknown, key: unknown) => {
tmpOutput.push(` ${JSON.stringify(key)} => ${JSON.stringify(val)}`)
langTmp.queryStr = `$request->setQuery(new http\\QueryString(array(\r\n${tmpOutput.join(
',\r\n'
)}\r\n)));`
langTmp.queryStr = `$request->setQuery(new http\\QueryString(array(\r\n${tmpOutput.join(',\r\n')}\r\n)));`
})
if (multipart) {
code =
@@ -534,7 +521,7 @@ export function generateCode(
'$request->setBody($body);\r\n\r\n' +
`$request->getBody()->${langTmp.paramsStr}\r\n\r\n` +
'$request->setHeaders(array(\r\n' +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
' "Content-Type":"multipart/form-data"\r\n' +
'));\r\n\r\n' +
'$client->enqueue($request)->send();\r\n' +
@@ -552,7 +539,7 @@ export function generateCode(
'$request->setBody($body);\r\n\r\n' +
`${langTmp.queryStr ? `${langTmp.queryStr}\r\n\r\n` : ''}` +
'$request->setHeaders(array(\r\n' +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
'));\r\n\r\n' +
'$client->enqueue($request)->send();\r\n' +
'$response = $client->getResponse();\r\n\r\n' +
@@ -572,9 +559,7 @@ export function generateCode(
let tmpOutput = ''
params.map((val: unknown, key: number) => {
if (val.data_type === 'file') {
tmpOutput += ` "${val.name}" => new CURLFile($file_path)${
key === params.length - 1 ? '' : ','
}\r\n`
tmpOutput += ` "${val.name}" => new CURLFile($file_path)${key === params.length - 1 ? '' : ','}\r\n`
} else {
tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r`
}
@@ -614,7 +599,7 @@ export function generateCode(
` CURLOPT_CUSTOMREQUEST => "${method}",\r\n` +
` CURLOPT_POSTFIELDS => ${langTmp.paramsStr},\r\n` +
' CURLOPT_HTTPHEADER => array(\r\n' +
`${langTmp.headerStr ? `${langTmp.headerStr},\r\n` : ''}` +
`${langTmp.headerStr ? `${langTmp.headerStr},\r\n` : ''}` +
' "Content-Type:multipart/form-data"' +
'\r\n ),\r\n' +
'));\r\n\r\n' +
@@ -769,36 +754,33 @@ export function generateCode(
tmpOutput.push(`${JSON.stringify(key)} : ${JSON.stringify(val)}`)
// langTmp.querystring = `querystring={${tmpOutput.join(',')}};`
langTmp.querystring = `{${tmpOutput.join(',')}}`
})
if(multipart) {
code =
'import requests \r\n\r\n' +
'headers = {\r\n' +
`${langTmp.headerStr}\r\n` +
'}\r\n' +
`url = "${method === 'GET' ? urlObj.origin + urlObj.pathname : urlObj.href}"\r\n` +
'//获取文件,需填路径 \r\n' +
"file_path = '' \r\n" +
`filename = "${langTmp.fileValue}" \r\n` +
`filetype = "${langTmp.fileType}" \r\n` +
'data = { \r\n' +
`${langTmp.paramsStr}\r\n` +
'} \r\n' +
'files = {"file": (filename, open(file_path, "rb"), filetype)} \r\n' +
`response=requests.${method.toLowerCase()}(url, files=files, headers=headers, data=data)\r\n` +
'print(response.text)\r\n'
if (multipart) {
code =
'import requests \r\n\r\n' +
'headers = {\r\n' +
`${langTmp.headerStr}\r\n` +
'}\r\n' +
`url = "${method === 'GET' ? urlObj.origin + urlObj.pathname : urlObj.href}"\r\n` +
'//获取文件,需填路径 \r\n' +
"file_path = '' \r\n" +
`filename = "${langTmp.fileValue}" \r\n` +
`filetype = "${langTmp.fileType}" \r\n` +
'data = { \r\n' +
`${langTmp.paramsStr}\r\n` +
'} \r\n' +
'files = {"file": (filename, open(file_path, "rb"), filetype)} \r\n' +
`response=requests.${method.toLowerCase()}(url, files=files, headers=headers, data=data)\r\n` +
'print(response.text)\r\n'
} else {
code =
'import requests\r\n\r\n' +
`url = "${method === 'GET' ? urlObj.origin + urlObj.pathname : urlObj.href}"\r\n\r\n` +
`payload = ${
langTmp.querystring ? langTmp.querystring : langTmp.paramsStr
}\r\n\r\n` +
`headers = {\r\n${langTmp.headerStr}` +
'\r\n}\r\n\r\n' +
`response=requests.request("${method}", url, ${payloadStr(method, headers)}, headers=headers)\r\n\r\n` +
'print(response.text)\r\n'
code =
'import requests\r\n\r\n' +
`url = "${method === 'GET' ? urlObj.origin + urlObj.pathname : urlObj.href}"\r\n\r\n` +
`payload = ${langTmp.querystring ? langTmp.querystring : langTmp.paramsStr}\r\n\r\n` +
`headers = {\r\n${langTmp.headerStr}` +
'\r\n}\r\n\r\n' +
`response=requests.request("${method}", url, ${payloadStr(method, headers)}, headers=headers)\r\n\r\n` +
'print(response.text)\r\n'
}
break
}
@@ -815,9 +797,7 @@ export function generateCode(
let tmpOutput = ''
params.map((val: unknown, key: number) => {
if (val.data_type === 'file') {
tmpOutput += ` "${val.name}" => new CURLFile($file_path)${
key === params.length - 1 ? '' : ','
}\r\n`
tmpOutput += ` "${val.name}" => new CURLFile($file_path)${key === params.length - 1 ? '' : ','}\r\n`
} else {
tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r`
}
@@ -847,7 +827,7 @@ export function generateCode(
`request = Net::HTTP::${method.toLowerCase().replace(/^\S/, (s: string) => {
return s?.toUpperCase()
})}.new(url)\r\n` +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
`request.body = ${langTmp.paramsStr}\r\n\r\n` +
'response = http.request(request)\r\n' +
'puts response.read_body'
@@ -883,25 +863,19 @@ export function generateCode(
}
}
code =
`${
requestType === 'JSON' ? `printf '${langTmp.paramsStr}'|` : ''
} http ${
(requestType || 'FORMDATA').toString() === 'FORMDATA' &&
!multipart
`${requestType === 'JSON' ? `printf '${langTmp.paramsStr}'|` : ''} http ${
(requestType || 'FORMDATA').toString() === 'FORMDATA' && !multipart
? '--form'
: (requestType || 'JSON').toString() === 'JSON' &&
!multipart
? '--follow'
: ''
: (requestType || 'JSON').toString() === 'JSON' && !multipart
? '--follow'
: ''
} ${multipart ? '--ignore-stdin --form --follow' : ''} ${method} '${
urlObj.href
}' ${multipart ? '\\\r' : '\\'}` +
`${multipart ? langTmp.paramsStr : ''}\r` +
`${langTmp.headerStr}` +
`${
(requestType || 'FORMDATA').toString() === 'FORMDATA' &&
!multipart &&
langTmp.paramsStr
(requestType || 'FORMDATA').toString() === 'FORMDATA' && !multipart && langTmp.paramsStr
? ` \\\r\n${langTmp.paramsStr}`
: ''
}`
@@ -923,7 +897,6 @@ export function generateCode(
}
})
langTmp.paramsStr = tmpOutput
} else {
const tmpOutput = parseFormData(params, {
format: '${name}=${value}',
@@ -1024,7 +997,7 @@ export function generateCode(
' if err != nil {\r\n' +
' return nil, err\r\n' +
' }\r\n' +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
' req.Header.Add("Content-Type", writer.FormDataContentType())\r\n\r\n' +
' client := &http.Client{}\r\n' +
' resp, err := client.Do(req)\r\n' +
@@ -1071,7 +1044,7 @@ export function generateCode(
}
default: {
langTmp.paramsStr = requestParam ? JSON.stringify(requestParam) : ''
code =
code =
'package main\r\n\r\n' +
'import (\r\n' +
' "bytes"\r\n' +
@@ -1091,7 +1064,7 @@ export function generateCode(
'func request() ([]byte, error) {\r\n' +
` uri := "${urlObj.href}"\r\n\r\n` +
` ${
langTmp.paramsStr
langTmp.paramsStr
? ` payload := map[string]interface{}${langTmp.paramsStr}`
: ' payload := strings.NewReader("")'
}\r\n\r\n` +
@@ -1116,7 +1089,7 @@ export function generateCode(
map: stringifyHeaders
})
let mediaType = 'application/octet-stream'
switch ( (requestType || 'FORMDATA').toUpperCase()) {
switch ((requestType || 'FORMDATA').toUpperCase()) {
case 'FORMDATA': {
if (multipart) {
mediaType = 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW'
@@ -1154,8 +1127,8 @@ export function generateCode(
break
}
}
if(multipart) {
code =
if (multipart) {
code =
'OkHttpClient client = new OkHttpClient();\r\n\r\n' +
'//获取文件,需填路径 \r\n' +
'File file = new File(""); \r\n\r\n' +
@@ -1175,11 +1148,7 @@ export function generateCode(
code =
'OkHttpClient client = new OkHttpClient().newBuilder().build();\r\n' +
`MediaType mediaType = MediaType.parse("${mediaType}");\r\n` +
`${
method === 'GET'
? ''
: `RequestBody body = RequestBody.create(mediaType, ${langTmp.paramsStr});\r\n`
}` +
`${method === 'GET' ? '' : `RequestBody body = RequestBody.create(mediaType, ${langTmp.paramsStr});\r\n`}` +
'Request request = new Request.Builder()\r\n' +
` .url("${urlObj.href}")\r\n` +
` .method("${method}",${method === 'GET' ? 'null' : 'body'})\r\n` +
@@ -1188,7 +1157,7 @@ export function generateCode(
'Response response = client.newCall(request).execute();\r\n' +
'System.out.println(response.body().string());\r\n'
}
break
}
@@ -1233,9 +1202,7 @@ export function generateCode(
`${indent}"header": {\r\n` +
`${langTmp.headerStr}\r\n` +
`${indent}},\r\n` +
`${
multipart ? `${indent}"processData": false,\r\n${indent}"contentType": false,\r\n` : ''
}` +
`${multipart ? `${indent}"processData": false,\r\n${indent}"contentType": false,\r\n` : ''}` +
`${langTmp.paramsStr ? `${indent}"data": data,\r\n` : ''}` +
`${indent}"success": (response)=> {\r\n` +
`${indent + indent}console.log(response.data)\r\n` +
@@ -1,218 +1,228 @@
import { useEffect, useMemo } from 'react';
import { Cascader } from 'antd';
import CODE_LANG from '@common/const/code/const';
import type { DefaultOptionType } from 'antd/es/cascader';
import { useState } from 'react';
import { cloneDeep } from 'lodash-es';
import { paramsJsonType } from './code-snippets.type';
import { DOMAIN_SUFIX } from './code-example.type';
import { transfromUrlParam } from './transform';
import { generateCode } from './generate-code';
import {ApiDetail} from "@common/const/api-detail";
import {Codebox} from "@common/components/postcat//api/Codebox";
import {Collapse} from "@common/components/postcat/api/Collapse";
import {Box} from "@mui/material";
import { $t } from '@common/locales';
import { useGlobalContext } from '@common/contexts/GlobalStateContext';
import { useEffect, useMemo } from 'react'
import { Cascader } from 'antd'
import CODE_LANG from '@common/const/code/const'
import type { DefaultOptionType } from 'antd/es/cascader'
import { useState } from 'react'
import { cloneDeep } from 'lodash-es'
import { paramsJsonType } from './code-snippets.type'
import { DOMAIN_SUFIX } from './code-example.type'
import { transfromUrlParam } from './transform'
import { generateCode } from './generate-code'
import { ApiDetail } from '@common/const/api-detail'
import { Codebox } from '@common/components/postcat//api/Codebox'
import { Collapse } from '@common/components/postcat/api/Collapse'
import { Box } from '@mui/material'
import { $t } from '@common/locales'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
type CodeSnippetCompoType = {
title:string
api:ApiDetail,
extraTitle:unknown,
extraContent:unknown,
minLines:number
title: string
api: ApiDetail
extraTitle: unknown
extraContent: unknown
minLines: number
}
const file: unknown[] = []
const env: unknown = {}
const loading: boolean = false
const codeMode: string = 'rust'
const codeMens: string[] = ['reset', 'copy', 'download', 'newTab', 'search']
const DOMAIN_REGEX: RegExp = new RegExp(
`^(((http|ftp|https):\/\/)|)(([\\\w\\\-_]+([\\\w\\\-\\\.]*)?(\\\.(${DOMAIN_SUFIX.join(
'|'
)})))|((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(localhost))((\\\/)|(\\\?)|(:)|($))`
)
const file: unknown[] = []
const env: unknown = {}
const loading: boolean = false
const codeMode: string = 'rust'
const codeMens: string[] = ['reset', 'copy', 'download', 'newTab', 'search']
const DOMAIN_REGEX: RegExp = new RegExp(
`^(((http|ftp|https):\/\/)|)(([\\\w\\\-_]+([\\\w\\\-\\\.]*)?(\\\.(${DOMAIN_SUFIX.join(
'|'
)})))|((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(localhost))((\\\/)|(\\\?)|(:)|($))`
)
let isMultipart: boolean = false
let isMultipart: boolean = false
export default function CodeSnippetCompo({title,api, extraTitle, extraContent, minLines=15}: CodeSnippetCompoType) {
const {state } = useGlobalContext()
// const [tokenState ] = useTokenBasicInfo()
const pretreatmentRequestInfo = (apiDoc: ApiDetail) =>{
isMultipart = false
const result: ApiDetail = cloneDeep(apiDoc)
const files: unknown = file || []
let isMuti: boolean = false
const headers: string[] = []
let alreadyHadContentType: boolean = false
result.headers = []
const originHeader = apiDoc.requestParams?.headerParams
//处理请求头部
originHeader?.forEach((header: unknown) => {
// if (
// tokenState?.selected_X_apibee_token &&
// originHeader.length &&
// originHeader[0].name == 'X-APISpace-Token'
// ) {
// originHeader[0].value = tokenState?.selected_X_apibee_token
// }
const { checkbox, name } = header
if ((checkbox || !header.hasOwnProperty('checkbox')) && name) {
headers.push(name?.toLowerCase())
result.headers.push(header)
if (/content-type/i.test(name)) {
alreadyHadContentType = true
}
export default function CodeSnippetCompo({
title,
api,
extraTitle,
extraContent,
minLines = 15
}: CodeSnippetCompoType) {
const { state } = useGlobalContext()
// const [tokenState ] = useTokenBasicInfo()
const pretreatmentRequestInfo = (apiDoc: ApiDetail) => {
isMultipart = false
const result: ApiDetail = cloneDeep(apiDoc)
const files: unknown = file || []
let isMuti: boolean = false
const headers: string[] = []
let alreadyHadContentType: boolean = false
result.headers = []
const originHeader = apiDoc.requestParams?.headerParams
//处理请求头部
originHeader?.forEach((header: unknown) => {
// if (
// tokenState?.selected_X_apibee_token &&
// originHeader.length &&
// originHeader[0].name == 'X-APISpace-Token'
// ) {
// originHeader[0].value = tokenState?.selected_X_apibee_token
// }
const { checkbox, name } = header
if ((checkbox || !header.hasOwnProperty('checkbox')) && name) {
headers.push(name?.toLowerCase())
result.headers.push(header)
if (/content-type/i.test(name)) {
alreadyHadContentType = true
}
})
const query: unknown = {}
}
})
const query: unknown = {}
apiDoc.requestParams?.queryParams?.forEach((query: unknown) => {
const { checkbox, name } = query
if ((checkbox || !query.hasOwnProperty('checkbox')) && name) {
query[name] = query?.paramAttr.example || ''
apiDoc.requestParams?.queryParams?.forEach((query: unknown) => {
const { checkbox, name } = query
if ((checkbox || !query.hasOwnProperty('checkbox')) && name) {
query[name] = query?.paramAttr.example || ''
}
})
result.URL = transfromUrlParam(result.uri, query)
//处理 restful 参数
apiDoc.requestParams?.restParams?.forEach((rest: unknown) => {
if ((rest.checkbox || !rest.hasOwnProperty('checkbox')) && rest.name && rest.paramAttr.example) {
if (eval(`/:${rest.name}/`).test(result.URL.trim())) {
result.URL = result.URL.replaceAll(`:${rest.name}`, rest.paramAttr.example)
} else if (
result.URL.trim().indexOf(`{{${rest.name}}}`) == -1 &&
result.URL.trim().indexOf(`{${rest.name}}`) > -1
) {
result.URL = result.URL.replaceAll(`{${rest.name}}`, rest.paramAttr.example)
}
})
result.URL = transfromUrlParam(result.uri, query)
}
})
//处理 restful 参数
apiDoc.requestParams?.restParams?.forEach((rest: unknown) => {
if ((rest.checkbox || !rest.hasOwnProperty('checkbox')) && rest.name && rest.paramAttr.example) {
if (eval(`/:${rest.name}/`).test(result.URL.trim())) {
result.URL = result.URL.replaceAll(`:${rest.name}`, rest.paramAttr.example )
} else if (
result.URL.trim().indexOf(`{{${rest.name}}}`) == -1 &&
result.URL.trim().indexOf(`{${rest.name}}`) > -1
) {
result.URL = result.URL.replaceAll(`{${rest.name}}`, rest.paramAttr.example)
}
}
})
result.params = []
//为请求参数 中的header、reset、body、query 添加 value 和 valueQuery 的值
switch (result.requestParams?.bodyParams?.[0]?.contentType) {
case 0: {
result.requestParams?.bodyParams?.forEach((body: unknown, key: unknown) => {
if ((body.checkbox || !body.hasOwnProperty('checkbox')) && body.name) {
if (paramsJsonType[body.dataType] == 'string' && body.paramAttr.example) {
isMuti = true
body.files = files[key] || []
}
result.params.push(body)
result.params = []
//为请求参数 中的header、reset、body、query 添加 value 和 valueQuery 的值
switch (result.requestParams?.bodyParams?.[0]?.contentType) {
case 0: {
result.requestParams?.bodyParams?.forEach((body: unknown, key: unknown) => {
if ((body.checkbox || !body.hasOwnProperty('checkbox')) && body.name) {
if (paramsJsonType[body.dataType] == 'string' && body.paramAttr.example) {
isMuti = true
body.files = files[key] || []
}
result.params.push(body)
}
})
if (!alreadyHadContentType) {
if (isMuti) {
isMultipart = true
result.headers.push({
name: 'Content-Type',
value: 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',
checkbox: true
})
} else {
result.headers.push({
name: 'Content-Type',
value: 'application/x-www-form-urlencoded',
checkbox: true
})
}
}
break
}
case 1: {
result.params = apiDoc.requestParams?.bodyParams
break
}
case 2: {
result.params = apiDoc.requestParams?.bodyParams
if (!alreadyHadContentType) {
result.headers.push({
name: 'Content-Type',
value: 'application/json',
checkbox: true
})
if (!alreadyHadContentType) {
if (isMuti) {
isMultipart = true
result.headers.push({
name: 'Content-Type',
value: 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',
checkbox: true
})
} else {
result.headers.push({
name: 'Content-Type',
value: 'application/x-www-form-urlencoded',
checkbox: true
})
}
}
break
}
case 1: {
result.params = apiDoc.requestParams?.bodyParams
break
}
case 2: {
result.params = apiDoc.requestParams?.bodyParams
if (!alreadyHadContentType) {
result.headers.push({
name: 'Content-Type',
value: 'application/json',
checkbox: true
})
}
break
}
case 3: {
result.params = apiDoc.requestParams?.bodyParams
if (!alreadyHadContentType) {
result.headers.push({
name: 'Content-Type',
value: 'application/xml',
checkbox: true
})
}
break
}
break
}
case 3: {
result.params = apiDoc.requestParams?.bodyParams
if (!alreadyHadContentType) {
result.headers.push({
name: 'Content-Type',
value: 'application/xml',
checkbox: true
})
}
break
}
result.requestType = result.requestParams?.bodyParams?.[0]?.contentType || 0
return result
}
const [code, setCode] = useState<string>('')
const [lang, setLang] = useState<number[]>([20])
result.requestType = result.requestParams?.bodyParams?.[0]?.contentType || 0
return result
}
let tempCode = ''
const getCode = (language: number | string) => {
if (!['HTTPS', 'HTTP'].includes(api.protocol?.toUpperCase())) {
tempCode = $t('暂不支持生成非 HTTPS 或非 HTTP 协议的代码示例')
setCode(tempCode)
return
}
tempCode = generateCode(
language.toString(),
isMultipart,
pretreatmentRequestInfo(cloneDeep(api))
)
const [code, setCode] = useState<string>('')
const [lang, setLang] = useState<number[]>([20])
let tempCode = ''
const getCode = (language: number | string) => {
if (!['HTTPS', 'HTTP'].includes(api.protocol?.toUpperCase())) {
tempCode = $t('暂不支持生成非 HTTPS 或非 HTTP 协议的代码示例')
setCode(tempCode)
return
}
tempCode = generateCode(language.toString(), isMultipart, pretreatmentRequestInfo(cloneDeep(api)))
setCode(tempCode)
}
useEffect(() => {
if(!Object.keys(api).length) return
getCode(lang[lang.length -1 ])
}, [api])
useEffect(() => {
if (!Object.keys(api).length) return
getCode(lang[lang.length - 1])
}, [api])
const onChange = (value: number[], record: DefaultOptionType[]) => {
const num = value[value.length - 1]
setLang(value)
if (!Object.keys(api).length) return
getCode(num)
}
const onChange = (value: number[],record:DefaultOptionType[]) => {
const num = value[value.length - 1]
setLang(value)
if(!Object.keys(api).length) return
getCode(num)
};
const filter = (inputValue: string, path: DefaultOptionType[]) =>
path.some((option) => (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1)
const filter = (inputValue: string, path: DefaultOptionType[]) =>
path.some(
(option) => (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1,
);
const [placeholderTxt, setPlaceholderTxt] = useState($t('搜索编程语言...'))
const [selectItemTxt, setSelectItemTxt] = useState('')
const [placeholderTxt, setPlaceholderTxt] = useState($t('搜索编程语言...'))
const [selectItemTxt, setSelectItemTxt ] = useState('')
const codeLangOptions = useMemo(()=>CODE_LANG.map(x=>({...x, label:$t(x.label as string)})),[state.language])
return (
<Collapse title={title}>
<Box width="100%">
<>
<Codebox extraContent={<><span className="ml-[12px]">{$t('编程语言')}</span><Cascader
const codeLangOptions = useMemo(
() => CODE_LANG.map((x) => ({ ...x, label: $t(x.label as string) })),
[state.language]
)
return (
<Collapse title={title}>
<Box width="100%">
<>
<Codebox
extraContent={
<>
<span className="ml-[12px]">{$t('编程语言')}</span>
<Cascader
options={codeLangOptions}
onChange={(value,record) => onChange(value as unknown as number[],record)}
onChange={(value, record) => onChange(value as unknown as number[], record)}
placeholder={placeholderTxt}
value={lang} // 当前的值
showSearch={{ filter }}
size="small"
allowClear={false}
// onDropdownVisibleChange={value => openChange(value)}
/></>}
language={'javascript'} value={code} readOnly={true} height={'250px'} width={'100%'}/>
</>
</Box>
</Collapse>
)
}
/>
</>
}
language={'javascript'}
value={code}
readOnly={true}
height={'250px'}
width={'100%'}
/>
</>
</Box>
</Collapse>
)
}
@@ -1,6 +1,6 @@
import { cloneDeep } from 'lodash-es'
import { PARAM_KEY_REF_TYPE, PARAM_TYPE_REF_TYPE } from './code-snippets.type';
import { tranformJson, tranformXml } from './util';
import { PARAM_KEY_REF_TYPE, PARAM_TYPE_REF_TYPE } from './code-snippets.type'
import { tranformJson, tranformXml } from './util'
type LANG_TYPE = 'Java' | 'HTTP' | 'shellHttpie' | 'go' | 'NodeJSNative'
type PARAM_HEADER_TYPE = { name: string; value: string }
type PARSE_OPTS_TYPE = {
@@ -30,201 +30,199 @@ export const paramsJsonType: unknown = {
NULL: 'null'
}
/**
* @description 拼接地址栏query参数,返回url
* @param {string} url 地址栏url
* @param {Object} query 地址栏query对象
*/
export function transfromUrlParam(url: string, query: { [key: string]: string }) {
const querys: string[] = []
Object.entries(query).forEach((item: string[]) => {
querys.push(`${item[0]}=${item[1]}`)
})
if (!querys.length) return url
return `${url}${url.includes('?') ? '&' : '?'}${querys.join('&')}`
}
export function parseUri(protocol: string, url: string) {
if (!/((http:\/\/)|(https:\/\/))/.test(url)) {
url = (protocol == 'HTTPS' ? 'https://' : 'http://') + url
}
return url
/**
* @description 拼接地址栏query参数,返回url
* @param {string} url 地址栏url
* @param {Object} query 地址栏query对象
*/
export function transfromUrlParam(url: string, query: { [key: string]: string }) {
const querys: string[] = []
Object.entries(query).forEach((item: string[]) => {
querys.push(`${item[0]}=${item[1]}`)
})
if (!querys.length) return url
return `${url}${url.includes('?') ? '&' : '?'}${querys.join('&')}`
}
export function parseUri(protocol: string, url: string) {
if (!/((http:\/\/)|(https:\/\/))/.test(url)) {
url = (protocol == 'HTTPS' ? 'https://' : 'http://') + url
}
return url
}
/**
* @description 处理FormData格式请求参数
* @param {Object} options {format:生成Formdata格式[option]separator:组合字符串的分割符[option]}
* @param {Array} params 待拼接数组
*/
export function parseFormData(params: unknown, { map, init, format, separator, langType, hasFileParams }: PARSE_OPTS_TYPE = {}) {
if (map) params = cloneDeep(params)
if (init) params = init(params)
if (format) {
//x-www
const result: unknown = []
params.map((val: unknown) => {
if (map) val = map(val)
result.push(format.replace('${name}', val.name).replace('${value}', val.value))
})
return result.join(separator || '&')
}
//multipart
let result: string = ''
const boundary: string = 'WebKitFormBoundary7MA4YWxkTrZu0gW'
params.forEach((val: unknown) => {
/**
* @description 处理FormData格式请求参数
* @param {Object} options {format:生成Formdata格式[option]separator:组合字符串的分割符[option]}
* @param {Array} params 待拼接数组
*/
export function parseFormData(
params: unknown,
{ map, init, format, separator, langType, hasFileParams }: PARSE_OPTS_TYPE = {}
) {
if (map) params = cloneDeep(params)
if (init) params = init(params)
if (format) {
//x-www
const result: unknown = []
params.map((val: unknown) => {
if (map) val = map(val)
if (val.files) {
if (val.files.length) {
result += `------${boundary}\r\n`
val.files.map((childVal: unknown) => {
result += `content-disposition: form-data; name="${val.name}"; filename="${val.value}"\r\n`
if (typeof childVal === 'string') {
result += `Content-Type: ${((childVal.match(/data:(.*);/) || [])[0] || '')
.replace(/^data:/, '')
.replace(/;$/, '')}\r\n`
} else {
result += `Content-Type: ${(childVal.dataUrl.match(/data:(.*);/)[0] || '')
.replace(/^data:/, '')
.replace(/;$/, '')}\r\n`
}
result += '\r\n'
})
}
} else {
result += `------${boundary}\r\n`
result += `content-disposition: form-data; name="${val.name}"\r\n`
result += '\r\n'
result += `${val.value}\r\n`
}
result.push(format.replace('${name}', val.name).replace('${value}', val.value))
})
result += `------${boundary}--`
return result
return result.join(separator || '&')
}
export function parseFileValue (params: unknown[]) {
const tmp = {
output: ''
}
if (params && params.length) {
for (let i = 0; i < params.length; i++) {
if (params[i].data_type === 'file') {
tmp.output = params[i].value || ''
break
}
//multipart
let result: string = ''
const boundary: string = 'WebKitFormBoundary7MA4YWxkTrZu0gW'
params.forEach((val: unknown) => {
if (map) val = map(val)
if (val.files) {
if (val.files.length) {
result += `------${boundary}\r\n`
val.files.map((childVal: unknown) => {
result += `content-disposition: form-data; name="${val.name}"; filename="${val.value}"\r\n`
if (typeof childVal === 'string') {
result += `Content-Type: ${((childVal.match(/data:(.*);/) || [])[0] || '')
.replace(/^data:/, '')
.replace(/;$/, '')}\r\n`
} else {
result += `Content-Type: ${(childVal.dataUrl.match(/data:(.*);/)[0] || '')
.replace(/^data:/, '')
.replace(/;$/, '')}\r\n`
}
result += '\r\n'
})
}
return tmp.output
}
}
export function payloadStr (method: string, headers: unknown[]) {
let tmpStr = ''
if (method === 'GET') {
tmpStr = 'params=payload'
} else {
headers.forEach((item) => {
if (item.name === 'Content-Type') {
switch (item.description || item.value) {
case 'application/x-www-form-urlencoded':
tmpStr = 'data=payload'
break
case 'application/json':
tmpStr = 'data=json.dumps(payload)'
break
default: {
tmpStr = 'params=payload'
}
result += `------${boundary}\r\n`
result += `content-disposition: form-data; name="${val.name}"\r\n`
result += '\r\n'
result += `${val.value}\r\n`
}
})
result += `------${boundary}--`
return result
}
export function parseFileValue(params: unknown[]) {
const tmp = {
output: ''
}
if (params && params.length) {
for (let i = 0; i < params.length; i++) {
if (params[i].data_type === 'file') {
tmp.output = params[i].value || ''
break
}
}
return tmp.output
}
}
export function payloadStr(method: string, headers: unknown[]) {
let tmpStr = ''
if (method === 'GET') {
tmpStr = 'params=payload'
} else {
headers.forEach((item) => {
if (item.name === 'Content-Type') {
switch (item.description || item.value) {
case 'application/x-www-form-urlencoded':
tmpStr = 'data=payload'
break
case 'application/json':
tmpStr = 'data=json.dumps(payload)'
break
default: {
tmpStr = 'params=payload'
}
}
})
}
return tmpStr
}
export function goCodeParseFormData(params: unknown[]) {
if (!params.length) return
let output = ''
params.forEach((item) => {
output += `payload.Set("${item.name}", "${item.value}")\r\n `
})
return output
}
export function parseFileType (fileValue: string) {
const isPng = fileValue.endsWith('.png')
const isJpeg = fileValue.endsWith('.jpeg')
const isJpg = fileValue.endsWith('.jpg')
let result = 'image/jpeg'
if (isPng) {
result = 'image/png'
} else if (isJpg) {
result = 'image/jpeg'
} else if (isJpeg) {
result = 'image/jpeg'
}
return result
}
/**
* @description 处理请求头格式
* @param {Object} options {format:生成请求头格式[option]separator:组合字符串的分割符[option]}
* @param {Array} headers 待拼接数组
*/
export function parseHeaders(
headers: PARAM_HEADER_TYPE[],
{ map, filter, format, separator }: PARSE_OPTS_TYPE = {}
) {
const result = []
if (map) {
headers = cloneDeep(headers)
}
for (const key in headers) {
let val = headers[key]
if (map) {
val = map(val)
}
if (filter) {
if (filter(val)) {
result.push(format?.replace('${name}', val.name)?.replace('${value}', val.value))
}
} else {
})
}
return tmpStr
}
export function goCodeParseFormData(params: unknown[]) {
if (!params.length) return
let output = ''
params.forEach((item) => {
output += `payload.Set("${item.name}", "${item.value}")\r\n `
})
return output
}
export function parseFileType(fileValue: string) {
const isPng = fileValue.endsWith('.png')
const isJpeg = fileValue.endsWith('.jpeg')
const isJpg = fileValue.endsWith('.jpg')
let result = 'image/jpeg'
if (isPng) {
result = 'image/png'
} else if (isJpg) {
result = 'image/jpeg'
} else if (isJpeg) {
result = 'image/jpeg'
}
return result
}
/**
* @description 处理请求头格式
* @param {Object} options {format:生成请求头格式[option]separator:组合字符串的分割符[option]}
* @param {Array} headers 待拼接数组
*/
export function parseHeaders(headers: PARAM_HEADER_TYPE[], { map, filter, format, separator }: PARSE_OPTS_TYPE = {}) {
const result = []
if (map) {
headers = cloneDeep(headers)
}
for (const key in headers) {
let val = headers[key]
if (map) {
val = map(val)
}
if (filter) {
if (filter(val)) {
result.push(format?.replace('${name}', val.name)?.replace('${value}', val.value))
}
} else {
result.push(format?.replace('${name}', val.name)?.replace('${value}', val.value))
}
return result.join(separator || '')
}
const keyRefs: PARAM_KEY_REF_TYPE = {
key: 'name',
type: 'data_type',
value: 'value',
childKey: 'child_list',
arrayItemKey: 'isArrItem'
}
return result.join(separator || '')
}
const keyRefs: PARAM_KEY_REF_TYPE = {
key: 'name',
type: 'data_type',
value: 'value',
childKey: 'child_list',
arrayItemKey: 'isArrItem'
}
const typeRefs: PARAM_TYPE_REF_TYPE = paramsJsonType
const typeRefs: PARAM_TYPE_REF_TYPE = paramsJsonType
export const parseRequestBodyToString = ({ requestType, params, apiRequestParamJsonType, raw }: unknown) => {
let result: string = ''
switch ((requestType || 'FORAMDATA').toString()) {
case 'RAW': {
//raw
// todo
result = raw
break
}
case 'JSON': {
//json
if (!params[0]?.hasOwnProperty('value')) keyRefs.value = 'name'
result = tranformJson(params, keyRefs, typeRefs)
if (apiRequestParamJsonType === 'ARRAY') {
//array
result = `[${result}]`
}
break
}
case 'XML': {
//xml
if (!params[0]?.hasOwnProperty('value')) keyRefs.value = 'name'
result = tranformXml(params, keyRefs, typeRefs)
break
}
export const parseRequestBodyToString = ({ requestType, params, apiRequestParamJsonType, raw }: unknown) => {
let result: string = ''
switch ((requestType || 'FORAMDATA').toString()) {
case 'RAW': {
//raw
// todo
result = raw
break
}
return result
}
case 'JSON': {
//json
if (!params[0]?.hasOwnProperty('value')) keyRefs.value = 'name'
result = tranformJson(params, keyRefs, typeRefs)
if (apiRequestParamJsonType === 'ARRAY') {
//array
result = `[${result}]`
}
break
}
case 'XML': {
//xml
if (!params[0]?.hasOwnProperty('value')) keyRefs.value = 'name'
result = tranformXml(params, keyRefs, typeRefs)
break
}
}
return result
}
@@ -1,19 +1,25 @@
import {Random} from 'mockjs'
import { PARAM_KEY_REF_TYPE, PARAM_LIST_TYPE, PARAM_LIS_ITEM_TYPE, PARAM_TYPE, PARAM_TYPE_REF_TYPE } from "./code-snippets.type"
import { Random } from 'mockjs'
import {
PARAM_KEY_REF_TYPE,
PARAM_LIST_TYPE,
PARAM_LIS_ITEM_TYPE,
PARAM_TYPE,
PARAM_TYPE_REF_TYPE
} from './code-snippets.type'
const DEFAULT_PARAM_KEY_REF: PARAM_KEY_REF_TYPE = {
key: 'key',
type: 'type',
value: 'value'
}
/**
* 将自定义列表转换为 xml
* @param list 列表
* @param keyRefs 关键词映射
* @param random 是否随机值
* @returns xml 字符串
*/
export function tranformXml(
/**
* 将自定义列表转换为 xml
* @param list 列表
* @param keyRefs 关键词映射
* @param random 是否随机值
* @returns xml 字符串
*/
export function tranformXml(
list: PARAM_LIST_TYPE,
keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF,
typeRefs: PARAM_TYPE_REF_TYPE = {},
@@ -74,40 +80,40 @@ export function tranformJson(
return `{${result.join(',')}}`
}
/**
* 将自定义列表转换为地址栏参数
* @param list 列表
* @param keyRefs 关键词映射
* @param random 是否随机值
* @returns key-value 结构字符串
*/
export function tranformUrlParam(
list: PARAM_LIST_TYPE,
keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF,
typeRefs: PARAM_TYPE_REF_TYPE = {},
random: boolean = false
) {
const { key, value, filter, type } = keyRefs
const result: string[] = []
list.forEach((item: unknown) => {
if (filter && item[filter]) return
const tab: string = item[key]
if (!tab) return
const itemType: PARAM_TYPE = typeRefs[item[type]]
/**
* 将自定义列表转换为地址栏参数
* @param list 列表
* @param keyRefs 关键词映射
* @param random 是否随机值
* @returns key-value 结构字符串
*/
export function tranformUrlParam(
list: PARAM_LIST_TYPE,
keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF,
typeRefs: PARAM_TYPE_REF_TYPE = {},
random: boolean = false
) {
const { key, value, filter, type } = keyRefs
const result: string[] = []
list.forEach((item: unknown) => {
if (filter && item[filter]) return
const tab: string = item[key]
if (!tab) return
const itemType: PARAM_TYPE = typeRefs[item[type]]
const text: string = random === true ? getRandomDataByType(itemType) : item[value]
result.push(`${tab}=${text}`)
})
return result.join('&') //分隔符为&
}
/**
* 将自定义列表转换为 key-value 结构
* @param list 列表
* @param keyRefs 关键词映射
* @param random 是否随机值
* @returns
*/
export function tranformKeyValue(
const text: string = random === true ? getRandomDataByType(itemType) : item[value]
result.push(`${tab}=${text}`)
})
return result.join('&') //分隔符为&
}
/**
* 将自定义列表转换为 key-value 结构
* @param list 列表
* @param keyRefs 关键词映射
* @param random 是否随机值
* @returns
*/
export function tranformKeyValue(
list: PARAM_LIST_TYPE,
keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF,
typeRefs: PARAM_TYPE_REF_TYPE = {},
@@ -1,112 +1,147 @@
import {useState, useEffect, useImperativeHandle} from 'react';
import {AutoComplete, Empty, Tabs} from 'antd';
import { ResultListType} from "@common/const/api-detail";
import {Collapse} from "@common/components/postcat/api/Collapse";
import {Box} from "@mui/material";
import {Codebox} from "@common/components/postcat/api/Codebox";
import { cloneDeep } from 'lodash-es';
import { $t } from '@common/locales';
import { useState, useEffect, useImperativeHandle } from 'react'
import { AutoComplete, Empty, Tabs } from 'antd'
import { ResultListType } from '@common/const/api-detail'
import { Collapse } from '@common/components/postcat/api/Collapse'
import { Box } from '@mui/material'
import { Codebox } from '@common/components/postcat/api/Codebox'
import { cloneDeep } from 'lodash-es'
import { $t } from '@common/locales'
export interface ResponseExampleCompoEditorApi {
getData: () => ResultListType[] | []
getData: () => ResultListType[] | []
}
const DEFAULT_RESULT_LIST = [
{id:'success',name:$t('成功示例'),httpCode:'200',content:''},
{id:'failed',name:$t('失败示例'),httpCode:'200',content:''},
{ id: 'success', name: $t('成功示例'), httpCode: '200', content: '' },
{ id: 'failed', name: $t('失败示例'), httpCode: '200', content: '' }
]
export const HTTP_STATUS_CODE = ['200', '403', '404', '410', '422', '500', '502', '503', '504']
export const CONTENT_TYPE_TYPE = [
'application/json',
'application/x-www-form-urlencoded',
'image/jpeg',
'image/png',
'multipart/form-data',
'text/asp',
'text/css',
'text/html',
'text/html; charset=UTF-8',
'text/plain',
'text/xml'
'application/json',
'application/x-www-form-urlencoded',
'image/jpeg',
'image/png',
'multipart/form-data',
'text/asp',
'text/css',
'text/html',
'text/html; charset=UTF-8',
'text/plain',
'text/xml'
]
export function ResponseExampleCompo ({ editorRef,title,detail,mode='view' }: {editorRef?: React.RefObject<ResponseExampleCompoEditorApi>,title:string, detail:resultList[]}) {
const [resultDemos, setResultDemos] = useState<unknown>([]);
export function ResponseExampleCompo({
editorRef,
title,
detail,
mode = 'view'
}: {
editorRef?: React.RefObject<ResponseExampleCompoEditorApi>
title: string
detail: resultList[]
}) {
const [resultDemos, setResultDemos] = useState<unknown>([])
useImperativeHandle(editorRef, () => ({
getData: () => {
return resultDemos||[]
}
}))
useImperativeHandle(editorRef, () => ({
getData: () => {
return resultDemos || []
}
}))
useEffect(() => {
if(mode === 'view'){
setResultDemos(detail);
}else{
setResultDemos(detail?.length > 0 ? detail: cloneDeep(DEFAULT_RESULT_LIST))
}
}, [detail]);
const updateResultList = (id:string, type:'httpCode' | 'httpContentType'|'content',value:string) => {
setResultDemos((prevList)=>{
for(let i = 0 ; i < prevList.length; i++){
if(prevList[i].id === id){
prevList[i][type] = value
return prevList
}
}
})
if (mode === 'view') {
setResultDemos(detail)
} else {
setResultDemos(detail?.length > 0 ? detail : cloneDeep(DEFAULT_RESULT_LIST))
}
}, [detail])
const updateResultList = (id: string, type: 'httpCode' | 'httpContentType' | 'content', value: string) => {
setResultDemos((prevList) => {
for (let i = 0; i < prevList.length; i++) {
if (prevList[i].id === id) {
prevList[i][type] = value
return prevList
}
}
})
}
return (
<Collapse title={title}>
<Box width="100%">
<Collapse title={title}>
<Box width="100%">
<Tabs className=" small-tabs" defaultActiveKey={resultDemos?.[0]?.id}>
{resultDemos && resultDemos?.map((item:ResultListType) => (
<Tabs.TabPane key={item.id} tab={item.name}>
<div >
<div className="ml-[8px] mb-[8px] flex">
{mode === 'view' ?
item.content ? <span className="mr-btnbase py-[5px] px-btnbase text-DEFAULT bg-[#f7f8fa] rounded border-[1px] border-solid border-DEFAULT"> HTTP Status Code: {item.httpCode}</span>:''
: <AutoComplete
className="mr-btnbase rounded "
options={HTTP_STATUS_CODE.map((code)=>({label:code, value:code}))}
style={{ width: 200 }}
value={item.httpCode}
status={item.httpCode ? '' : 'error'}
onSelect={(value)=>updateResultList(item.id,'httpCode',value)}
placeholder={$t("HTTP 状态码")}
/>
}
{mode === 'view' ?
item.content ? <span className="mr-btnbase py-[5px] px-btnbase text-DEFAULT bg-[#f7f8fa] rounded border-[1px] border-solid border-DEFAULT">Content-Type: {item.httpContentType || 'text/html;charset=UTF-8'}</span>:''
: <AutoComplete
options={CONTENT_TYPE_TYPE.map((type)=>({label:type, value:type}))}
style={{ width: 200 }}
value={item.httpContentType || 'text/html;charset=UTF-8'}
onSelect={(value)=>updateResultList(item.id,'httpContentType',value)}
placeholder={$t("默认 text/html;charset=UTF-8")}
/>}
{resultDemos &&
resultDemos?.map((item: ResultListType) => (
<Tabs.TabPane key={item.id} tab={item.name}>
<div>
<div className="ml-[8px] mb-[8px] flex">
{mode === 'view' ? (
item.content ? (
<span className="mr-btnbase py-[5px] px-btnbase text-DEFAULT bg-[#f7f8fa] rounded border-[1px] border-solid border-DEFAULT">
{' '}
HTTP Status Code: {item.httpCode}
</span>
) : (
''
)
) : (
<AutoComplete
className="mr-btnbase rounded "
options={HTTP_STATUS_CODE.map((code) => ({ label: code, value: code }))}
style={{ width: 200 }}
value={item.httpCode}
status={item.httpCode ? '' : 'error'}
onSelect={(value) => updateResultList(item.id, 'httpCode', value)}
placeholder={$t('HTTP 状态码')}
/>
)}
{mode === 'view' ? (
item.content ? (
<span className="mr-btnbase py-[5px] px-btnbase text-DEFAULT bg-[#f7f8fa] rounded border-[1px] border-solid border-DEFAULT">
Content-Type: {item.httpContentType || 'text/html;charset=UTF-8'}
</span>
) : (
''
)
) : (
<AutoComplete
options={CONTENT_TYPE_TYPE.map((type) => ({ label: type, value: type }))}
style={{ width: 200 }}
value={item.httpContentType || 'text/html;charset=UTF-8'}
onSelect={(value) => updateResultList(item.id, 'httpContentType', value)}
placeholder={$t('默认 text/html;charset=UTF-8')}
/>
)}
</div>
{mode === 'view' ? (
<>
{item.content ? (
<pre className="border-[1px] border-solid border-BORDER p-[6px] rounded w-auto min-h-[130px] max-h-[500px] overflow-auto mt-[0px]">
{item.content}
</pre>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={$t('暂未填写示例')} />
)}
</>
) : (
<>
<Codebox
value={item.content}
language="json"
width="100%"
height={'250px'}
onChange={(value) => updateResultList(item.id, 'content', value)}
/>
</>
)}
</div>
{mode === 'view' ?
<>
{ item.content ?
<pre className="border-[1px] border-solid border-BORDER p-[6px] rounded w-auto min-h-[130px] max-h-[500px] overflow-auto mt-[0px]">{item.content}</pre>
:
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={$t("暂未填写示例")}/>
}</>
: <>
<Codebox value={item.content} language='json' width="100%" height={'250px'} onChange={(value)=>updateResultList(item.id,'content',value)}/>
</>
}
</div>
</Tabs.TabPane>
))}
</Tabs.TabPane>
))}
</Tabs>
</Box>
</Collapse>
);
</Box>
</Collapse>
)
}
@@ -1,259 +1,297 @@
import { Collapse } from './api/Collapse'
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { Select, Input, Space } from 'antd'
import { Box, Stack, ThemeProvider, createTheme } from '@mui/material'
import { ApiResponseEditor, ApiResponseEditorApi } from './api/ApiManager/components/ApiResponseEditor'
import { ApiRequestEditor, ApiRequestEditorApi } from './api/ApiManager/components/ApiRequestEditor'
import { ResponseExampleCompo, ResponseExampleCompoEditorApi } from '@common/components/apispace/response-example'
import { ResultListType } from '@common/const/api-detail'
import { SystemApiDetail, SystemInsideApiProxyHandle } from '@core/const/system/type'
import SystemInsideApiProxy from '@core/pages/system/api/SystemInsideApiProxy'
import ApiMatch from './api/ApiPreview/components/ApiMatch'
import { v4 as uuidv4 } from 'uuid'
import { PLACEHOLDER } from '@common/const/const'
import { $t } from '@common/locales'
import {Collapse} from "./api/Collapse";
import {forwardRef, useEffect, useImperativeHandle, useRef, useState} from "react";
import {Select,Input,Space} from "antd";
import { Box, Stack, ThemeProvider, createTheme } from "@mui/material"
import {ApiResponseEditor, ApiResponseEditorApi} from "./api/ApiManager/components/ApiResponseEditor";
import {ApiRequestEditor, ApiRequestEditorApi} from "./api/ApiManager/components/ApiRequestEditor";
import {ResponseExampleCompo, ResponseExampleCompoEditorApi} from "@common/components/apispace/response-example";
import {ResultListType} from "@common/const/api-detail";
import { SystemApiDetail, SystemInsideApiProxyHandle } from "@core/const/system/type";
import SystemInsideApiProxy from "@core/pages/system/api/SystemInsideApiProxy";
import ApiMatch from "./api/ApiPreview/components/ApiMatch";
import {v4 as uuidv4} from 'uuid'
import { PLACEHOLDER } from "@common/const/const";
import { $t } from "@common/locales";
const PROTOCOL_LIST = ['HTTP','HTTPS']
const HTTP_METHOD_LIST = ['POST','GET','PUT', 'DELETE','HEAD','OPTIONS','PATCH']
export interface ApiEditApi{
getData:()=>(Promise<{apiInfo:Partial<SystemApiDetail>}|string|boolean> | undefined)
const PROTOCOL_LIST = ['HTTP', 'HTTPS']
const HTTP_METHOD_LIST = ['POST', 'GET', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH']
export interface ApiEditApi {
getData: () => Promise<{ apiInfo: Partial<SystemApiDetail> } | string | boolean> | undefined
}
interface DescriptionHandle{
getData:()=>string
interface DescriptionHandle {
getData: () => string
}
interface ApiNameProps{
apiInfo:SystemApiDetail
interface ApiNameProps {
apiInfo: SystemApiDetail
}
interface ApiNameHandle{
getData:()=>string
interface ApiNameHandle {
getData: () => string
}
export const theme = createTheme({
palette: {
primary: {
main: '#3D46F2', // 自定义主色调
},
text: {
primary: '#333', // 主要文字颜色
secondary: '#333', // 次要文字颜色
},
// 添加其他颜色配置,如错误色、背景色等
error: {
main: '#d32f2f',
},
background: {
paper: '#fff',
default: '#f7f8fa',
},
palette: {
primary: {
main: '#3D46F2' // 自定义主色调
},
transitions:{
create:()=>'none'
text: {
primary: '#333', // 主要文字颜色
secondary: '#333' // 次要文字颜色
},
components:{
MuiInput: {
styleOverrides: {
root: {
'&::placeholder': {
color: '#BBB', // 设置 placeholder 的颜色
},
'&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error)': {
borderColor: '#3D46F2', // 设置 hover 时的边框颜色
borderWidth: '1px', // 设置边框粗细
},
'&.Mui-focused': {
borderColor: '#3D46F2', // 设置选中时的边框颜色
borderWidth: '1px', // 设置边框粗细
},
},
// 添加其他颜色配置,如错误色、背景色等
error: {
main: '#d32f2f'
},
background: {
paper: '#fff',
default: '#f7f8fa'
}
},
transitions: {
create: () => 'none'
},
components: {
MuiInput: {
styleOverrides: {
root: {
'&::placeholder': {
color: '#BBB' // 设置 placeholder 的颜色
},
},
MuiTextField: {
styleOverrides: {
root: {
'&::placeholder': {
color: '#BBB', // 设置 placeholder 的颜色
},
'&:hover .MuiOutlinedInput-notchedOutline':{
borderColor: '#3D46F2', // 设置选中时的边框颜色
borderWidth: '1px', // 设置边框粗细
}
'&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error)': {
borderColor: '#3D46F2', // 设置 hover 时的边框颜色
borderWidth: '1px' // 设置边框粗细
},
'&.Mui-focused': {
borderColor: '#3D46F2', // 设置选中时的边框颜色
borderWidth: '1px' // 设置边框粗细
}
}
}
},
MuiTextField: {
styleOverrides: {
root: {
'&::placeholder': {
color: '#BBB' // 设置 placeholder 的颜色
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: '#3D46F2', // 设置选中时的边框颜色
borderWidth: '1px' // 设置边框粗细
}
}
}
},
MuiCheckbox: {
styleOverrides: {
root: {
'&:hover': {
backgroundColor: 'transparent' // 设置 hover 时的背景色为透明
},
'&:hover:before': {
backgroundColor: 'transparent' // 确保不透明度也为透明
},
transition: 'none' // 取消过渡效果
}
}
},
MuiButton: {
styleOverrides: {
root: {
'&': {
marginLeft: '0px',
padding: '3px 12px',
borderRadius: '4px'
}
}
}
},
MuiSelect: {
styleOverrides: {
root: {
'&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error)': {
borderColor: '#3D46F2',
borderWidth: '1px'
},
'&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error) .MuiOutlinedInput-notchedOutline': {
borderColor: '#3D46F2',
borderWidth: '1px'
},
'&.Mui-focused': {
borderColor: '#3D46F2',
borderWidth: '1px'
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: '#3D46F2',
borderWidth: '1px'
}
}
}
},
MuiMenu: {
styleOverrides: {
root: {
'.MuiMenuItem-root:hover': {
backgroundColor: '#EBEEF2'
},
'.MuiMenuItem-root.Mui-selected': {
backgroundColor: '#EBEEF2'
}
}
}
},
MuiInputLabel: {
styleOverrides: {
root: {
color: '#BBB' // 设置 label 的颜色为灰色
}
}
}
}
})
export default function ApiEdit({
apiInfo,
editorRef,
loaded,
serviceId,
teamId
}: {
apiInfo: SystemApiDetail
editorRef?: React.RefObject<ApiEditApi>
loaded: boolean
serviceId: string
teamId: string
}) {
const requestRef = useRef<ApiRequestEditorApi>(null)
const responseRef = useRef<ApiResponseEditorApi>(null)
const resultListRef = useRef<ResponseExampleCompoEditorApi>(null)
const protocolOptionList = PROTOCOL_LIST.map((x) => ({ label: x, value: x }))
const methodOptionList = HTTP_METHOD_LIST.map((x) => ({ label: x, value: x }))
const [apiName, setApiName] = useState<string>('')
const [resultList, setResultList] = useState<ResultListType[]>([])
const proxyRef = useRef<SystemInsideApiProxyHandle>(null)
const descriptionRef = useRef<DescriptionHandle>(null)
const apiNameRef = useRef<ApiNameHandle>(null)
useImperativeHandle(editorRef, () => ({
getData: () => {
return proxyRef.current
?.validate()
.then((res) => {
const name = apiNameRef.current?.getData()
if (!name) return Promise.reject($t('请填写接口名称'))
const newData: { apiInfo: Partial<SystemApiDetail> } = {
apiInfo: {
info: {
name,
description: descriptionRef.current?.getData()
},
},
},
MuiCheckbox: {
styleOverrides: {
root: {
'&:hover': {
backgroundColor: 'transparent', // 设置 hover 时的背景色为透明
},
'&:hover:before': {
backgroundColor: 'transparent', // 确保不透明度也为透明
},
transition: 'none', // 取消过渡效果
},
},
},
MuiButton:{
styleOverrides: {
root: {
'&':{
marginLeft:'0px',
padding:'3px 12px',
borderRadius:'4px'
}
proxy: res,
doc: {
...apiInfo?.doc,
requestParams: requestRef.current!.getData()!,
responseList: responseRef.current!.getData()!,
resultList: resultListRef.current!.getData()!
}
}
},
MuiSelect: {
styleOverrides: {
root: {
'&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error)': {
borderColor: '#3D46F2',
borderWidth: '1px',
},
'&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error) .MuiOutlinedInput-notchedOutline': {
borderColor: '#3D46F2',
borderWidth: '1px',
},
'&.Mui-focused': {
borderColor: '#3D46F2',
borderWidth: '1px',
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: '#3D46F2',
borderWidth: '1px',
},
},
},
},
MuiMenu:{
styleOverrides: {
root:{
'.MuiMenuItem-root:hover':{
backgroundColor: '#EBEEF2',
},
'.MuiMenuItem-root.Mui-selected':{
backgroundColor: '#EBEEF2',
}
}
}
},
MuiInputLabel: {
styleOverrides: {
root: {
color: '#BBB', // 设置 label 的颜色为灰色
},
},
},
}
return Promise.resolve(newData)
})
.catch((errInfo) => Promise.reject(errInfo))
}
});
export default function ApiEdit({apiInfo,editorRef,loaded,serviceId, teamId}:{apiInfo:SystemApiDetail,editorRef?:React.RefObject<ApiEditApi>,loaded:boolean,serviceId:string, teamId:string}){
const requestRef = useRef<ApiRequestEditorApi>(null)
const responseRef = useRef<ApiResponseEditorApi>(null)
const resultListRef = useRef<ResponseExampleCompoEditorApi>(null)
const protocolOptionList = PROTOCOL_LIST.map((x)=>({label:x,value:x}))
const methodOptionList = HTTP_METHOD_LIST.map((x)=>({label:x,value:x}))
const [apiName,setApiName]=useState<string>('')
const [resultList,setResultList] = useState<ResultListType[]>([])
const proxyRef = useRef<SystemInsideApiProxyHandle>(null)
const descriptionRef = useRef<DescriptionHandle>(null)
const apiNameRef = useRef<ApiNameHandle>(null)
useImperativeHandle(editorRef, () => ({
getData: () => {
return proxyRef.current?.validate().then((res)=>{
const name = apiNameRef.current?.getData()
if(!name) return Promise.reject($t('请填写接口名称'))
const newData :{apiInfo:Partial<SystemApiDetail>}= {
apiInfo:{
info:{
name,
description:descriptionRef.current?.getData(),
},
proxy:res,
doc:{
...apiInfo?.doc,
requestParams:requestRef.current!.getData()!,
responseList:responseRef.current!.getData()!,
resultList:resultListRef.current!.getData()!
}
}
}
return Promise.resolve(newData)
}).catch((errInfo)=>Promise.reject(errInfo))
}
}))
useEffect(() => {
if (!apiInfo || Object.keys(apiInfo).length === 0) return
setApiName(apiInfo.name!)
setResultList(apiInfo?.doc?.resultList || [])
}, [apiInfo])
const Description = forwardRef<DescriptionHandle, { initDescription: string | undefined }>((props, ref) => {
const { initDescription } = props
const [description, setDescription] = useState<string>(initDescription || '')
useImperativeHandle(ref, () => ({
getData: () => description
}))
useEffect(() => {
if(!apiInfo || Object.keys(apiInfo).length === 0) return
setApiName(apiInfo.name!)
setResultList(apiInfo?.doc?.resultList || [])
}, [apiInfo]);
const Description = forwardRef<DescriptionHandle,{initDescription:string|undefined}>((props,ref)=>{
const { initDescription } = props
const [description, setDescription] = useState<string>(initDescription||'')
useImperativeHandle(ref, ()=>({
getData:()=>description
}))
return (
<Input.TextArea className="w-full border-none" value={description} onChange={(e)=>setDescription(e.target.value)} placeholder={$t(PLACEHOLDER.input)}/>
)
})
const ApiName = forwardRef<ApiNameHandle,ApiNameProps>((props,ref)=>{
const {apiInfo} = props
const [apiName, setApiName] = useState<string>(apiInfo?.name || '')
useImperativeHandle(ref, ()=>({
getData:()=>apiName
}))
return (
<>
<Space.Compact className="w-full mb-btnybase">
<Select className="w-[15%] min-w-[100px]" value={apiInfo?.protocol || 'HTTP'} disabled={true} options={protocolOptionList} />
<Select className="w-[15%] min-w-[100px]" value={apiInfo?.method} disabled={true} options={methodOptionList} />
<Input className="w-[70%]" value={apiInfo?.path} disabled={true} />
</Space.Compact>
<Input value={apiName} onChange={(e)=>setApiName(e.target.value) } status={apiName ? '' : 'error'}/>
</>
)
})
return(
<>
<ThemeProvider theme={theme}>
<Box>
<Box>
<Stack direction="column" spacing={3}>
<ApiName apiInfo={apiInfo} ref={apiNameRef}/>
<Collapse key="description" title={$t('详细说明')}>
<Description initDescription={apiInfo?.description} ref={descriptionRef}/>
</Collapse>
{
apiInfo?.match && apiInfo.match?.length > 0 &&
<ApiMatch title={$t('高级匹配')} rows={apiInfo?.match.map((x)=>{x.id = uuidv4();return x})} />
}
<Collapse title={$t('转发配置')} key="proxy" >
<SystemInsideApiProxy className="m-[12px] px-[12px]" initProxyValue={apiInfo?.proxy} serviceId={serviceId!} ref={proxyRef} />
</Collapse>
<Collapse title={$t('请求参数')} key="request" >
<ApiRequestEditor editorRef={requestRef} apiInfo={apiInfo?.doc} loaded={loaded} />
</Collapse>
<Collapse title={$t('返回值')} key="response">
<ApiResponseEditor editorRef={responseRef} apiInfo={apiInfo?.doc} loaded={loaded}/>
</Collapse>
<ResponseExampleCompo editorRef={resultListRef} mode='edit' title={$t('返回示例')} detail={resultList}/>
</Stack>
</Box>
</Box>
</ThemeProvider>
</>
return (
<Input.TextArea
className="w-full border-none"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={$t(PLACEHOLDER.input)}
/>
)
}
})
const ApiName = forwardRef<ApiNameHandle, ApiNameProps>((props, ref) => {
const { apiInfo } = props
const [apiName, setApiName] = useState<string>(apiInfo?.name || '')
useImperativeHandle(ref, () => ({
getData: () => apiName
}))
return (
<>
<Space.Compact className="w-full mb-btnybase">
<Select
className="w-[15%] min-w-[100px]"
value={apiInfo?.protocol || 'HTTP'}
disabled={true}
options={protocolOptionList}
/>
<Select
className="w-[15%] min-w-[100px]"
value={apiInfo?.method}
disabled={true}
options={methodOptionList}
/>
<Input className="w-[70%]" value={apiInfo?.path} disabled={true} />
</Space.Compact>
<Input value={apiName} onChange={(e) => setApiName(e.target.value)} status={apiName ? '' : 'error'} />
</>
)
})
return (
<>
<ThemeProvider theme={theme}>
<Box>
<Box>
<Stack direction="column" spacing={3}>
<ApiName apiInfo={apiInfo} ref={apiNameRef} />
<Collapse key="description" title={$t('详细说明')}>
<Description initDescription={apiInfo?.description} ref={descriptionRef} />
</Collapse>
{apiInfo?.match && apiInfo.match?.length > 0 && (
<ApiMatch
title={$t('高级匹配')}
rows={apiInfo?.match.map((x) => {
x.id = uuidv4()
return x
})}
/>
)}
<Collapse title={$t('转发配置')} key="proxy">
<SystemInsideApiProxy
className="m-[12px] px-[12px]"
initProxyValue={apiInfo?.proxy}
serviceId={serviceId!}
ref={proxyRef}
/>
</Collapse>
<Collapse title={$t('请求参数')} key="request">
<ApiRequestEditor editorRef={requestRef} apiInfo={apiInfo?.doc} loaded={loaded} />
</Collapse>
<Collapse title={$t('返回值')} key="response">
<ApiResponseEditor editorRef={responseRef} apiInfo={apiInfo?.doc} loaded={loaded} />
</Collapse>
<ResponseExampleCompo editorRef={resultListRef} mode="edit" title={$t('返回示例')} detail={resultList} />
</Stack>
</Box>
</Box>
</ThemeProvider>
</>
)
}
@@ -1,159 +1,169 @@
import {useCallback, useEffect, useState} from "react";
import Search from "antd/es/input/Search";
import {Button, Space, Tooltip} from "antd";
import CodeSnippetCompo from "@common/components/apispace/code-snippet";
import {ApiDetail} from "@common/const/api-detail";
import {flattenTree} from "@common/utils/postcat.tsx";
import MessageBodyComponent, {RenderMessageBody} from "./api/ApiPreview/components/MessageBody";
import HeaderFields from "./api/ApiPreview/components/HeaderFields";
import {ResponseExampleCompo} from "@common/components/apispace/response-example";
import {MoreSetting} from "./api/MoreSetting";
import {
useMoreSettingHiddenConfig
} from "./api/ApiManager/components/MessageDataGrid/hooks/useMoreSettingHiddenConfig.ts";
import {MessageType} from "./api/ApiManager/components/MessageDataGrid";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { ThemeProvider } from "@mui/material";
import { theme } from "./ApiEdit.tsx";
import { $t } from "@common/locales/index.ts";
import { useCallback, useEffect, useState } from 'react'
import Search from 'antd/es/input/Search'
import { Button, Space, Tooltip } from 'antd'
import CodeSnippetCompo from '@common/components/apispace/code-snippet'
import { ApiDetail } from '@common/const/api-detail'
import { flattenTree } from '@common/utils/postcat.tsx'
import MessageBodyComponent, { RenderMessageBody } from './api/ApiPreview/components/MessageBody'
import HeaderFields from './api/ApiPreview/components/HeaderFields'
import { ResponseExampleCompo } from '@common/components/apispace/response-example'
import { MoreSetting } from './api/MoreSetting'
import { useMoreSettingHiddenConfig } from './api/ApiManager/components/MessageDataGrid/hooks/useMoreSettingHiddenConfig.ts'
import { MessageType } from './api/ApiManager/components/MessageDataGrid'
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { ThemeProvider } from '@mui/material'
import { theme } from './ApiEdit.tsx'
import { $t } from '@common/locales/index.ts'
export const SearchBtn = ({entity}:{entity:unknown})=>{
return (
<Tooltip >
<span className="text-disabled">{$t('测试 API')}</span>
</Tooltip>
)
export const SearchBtn = ({ entity }: { entity: unknown }) => {
return (
<Tooltip>
<span className="text-disabled">{$t('测试 API')}</span>
</Tooltip>
)
}
export default function ApiPreview(props:{testClick?:()=>void, entity:ApiDetail}){
const {testClick,entity} = props
const {requestParams,responseList,resultList} = entity
const [requestBodyList, setRequestBodyList] = useState<RenderMessageBody[]>([])
const [responseBodyList, setResponseBodyList] = useState<RenderMessageBody[]>([])
const [currentMoreSettingParam, setCurrentMoreSettingParam] = useState<RenderMessageBody | null>(null)
export default function ApiPreview(props: { testClick?: () => void; entity: ApiDetail }) {
const { testClick, entity } = props
const { requestParams, responseList, resultList } = entity
const [requestBodyList, setRequestBodyList] = useState<RenderMessageBody[]>([])
const [responseBodyList, setResponseBodyList] = useState<RenderMessageBody[]>([])
const [currentMoreSettingParam, setCurrentMoreSettingParam] = useState<RenderMessageBody | null>(null)
// const responseData = responseList?.[0]
// const responseParams = responseData?.responseParams?.headerParams
// const responseData = responseList?.[0]
// const responseParams = responseData?.responseParams?.headerParams
useEffect(() => {
// setTimeout(()=>{
// const element = document.querySelectorAll('.MuiDataGrid-main');
// if(element?.length > 0){
// for(const x of element){
// x.childNodes[x.childNodes.length - 1 ].textContent === 'MUI X Missing license key' ? x.childNodes[x.childNodes.length - 1 ].textContent = '' :null
// }
// }
// },500)
useEffect(() => {
// setTimeout(()=>{
// const element = document.querySelectorAll('.MuiDataGrid-main');
// if(element?.length > 0){
// for(const x of element){
// x.childNodes[x.childNodes.length - 1 ].textContent === 'MUI X Missing license key' ? x.childNodes[x.childNodes.length - 1 ].textContent = '' :null
// }
// }
// },500)
setRequestBodyList(
flattenTree(requestParams?.bodyParams || [], 'childList', 'name') as unknown as RenderMessageBody[]
)
setResponseBodyList(
flattenTree(
responseList?.[0]?.responseParams?.bodyParams || [],
'childList',
'name'
) as unknown as RenderMessageBody[]
)
}, [requestParams, responseList, resultList])
setRequestBodyList(
flattenTree(requestParams?.bodyParams || [], 'childList', 'name') as unknown as RenderMessageBody[]
)
setResponseBodyList(
flattenTree(
responseList?.[0]?.responseParams?.bodyParams || [],
'childList',
'name'
) as unknown as RenderMessageBody[]
)
}, [requestParams,responseList,resultList]);
const handleCloseMoreSetting = useCallback(() => {
setCurrentMoreSettingParam(null)
}, [])
const moreSettingHiddenConfig = useMoreSettingHiddenConfig({
param: currentMoreSettingParam as unknown as RenderMessageBody,
// TODO:
messageType: 'Header' as MessageType,
readOnly: true
})
const handleCloseMoreSetting = useCallback(() => {
setCurrentMoreSettingParam(null)
}, [])
const handleTest = () => {
// testClick && testClick()
}
const moreSettingHiddenConfig = useMoreSettingHiddenConfig({
param: currentMoreSettingParam as unknown as RenderMessageBody,
// TODO:
messageType: 'Header' as MessageType,
readOnly: true
})
const handleTest = () => {
// testClick && testClick()
};
return (<>
<ThemeProvider theme={theme}>
{testClick &&
<Space direction="vertical" className="mb-btnybase w-full mt-btnybase">
return (
<>
<ThemeProvider theme={theme}>
{testClick && (
<Space direction="vertical" className="mb-btnybase w-full mt-btnybase">
<Search
readOnly
addonBefore={entity?.method}
value={entity?.uri}
// enterButton={<SearchBtn entity={entity}/>}
onSearch={handleTest}
readOnly
addonBefore={entity?.method}
value={entity?.uri}
// enterButton={<SearchBtn entity={entity}/>}
onSearch={handleTest}
/>
</Space>}
</Space>
)}
{
requestParams?.headerParams?.length > 0 &&
<HeaderFields title={$t('请求 Header')} rows={requestParams?.headerParams}
onMoreSettingChange={setCurrentMoreSettingParam} />
}
{requestParams?.headerParams?.length > 0 && (
<HeaderFields
title={$t('请求 Header')}
rows={requestParams?.headerParams}
onMoreSettingChange={setCurrentMoreSettingParam}
/>
)}
{requestBodyList?.length > 0 &&
<MessageBodyComponent
title={$t("请求 Body")}
rows={requestBodyList}
contentType={requestParams?.bodyParams[0]?.contentType}
onMoreSettingChange={setCurrentMoreSettingParam}
/>
}
{requestBodyList?.length > 0 && (
<MessageBodyComponent
title={$t('请求 Body')}
rows={requestBodyList}
contentType={requestParams?.bodyParams[0]?.contentType}
onMoreSettingChange={setCurrentMoreSettingParam}
/>
)}
{
requestParams?.queryParams?.length > 0 &&
<HeaderFields title={$t('Query 参数')} rows={requestParams?.queryParams}
onMoreSettingChange={setCurrentMoreSettingParam} />
}
{requestParams?.queryParams?.length > 0 && (
<HeaderFields
title={$t('Query 参数')}
rows={requestParams?.queryParams}
onMoreSettingChange={setCurrentMoreSettingParam}
/>
)}
{
requestParams?.restParams?.length > 0 &&
<HeaderFields title={$t('Rest 参数')} rows={requestParams?.restParams}
onMoreSettingChange={setCurrentMoreSettingParam}/>
}
{requestParams?.restParams?.length > 0 && (
<HeaderFields
title={$t('Rest 参数')}
rows={requestParams?.restParams}
onMoreSettingChange={setCurrentMoreSettingParam}
/>
)}
{/*<h3 className="text-lg mb-btnybase font-normal flex items-center">请求示例代码</h3>*/}
<CodeSnippetCompo
title={$t('请求示例代码')}
api={entity}
extraContent={ testClick ? <div className="ml-5">
<Tooltip >
<WithPermission access="" >
<Button type='primary' onClick={handleTest} size='small' className='w-[114px]'>{$t('测试 API')}</Button>
</WithPermission>
title={$t('请求示例代码')}
api={entity}
extraContent={
testClick ? (
<div className="ml-5">
<Tooltip>
<WithPermission access="">
<Button type="primary" onClick={handleTest} size="small" className="w-[114px]">
{$t('测试 API')}
</Button>
</WithPermission>
</Tooltip>
</div> : undefined }
</div>
) : undefined
}
/>
{resultList?.length > 0 && <ResponseExampleCompo title={$t('响应示例')} detail={resultList}/>}
{resultList?.length > 0 && <ResponseExampleCompo title={$t('响应示例')} detail={resultList} />}
{
responseList?.[0]?.responseParams?.headerParams?.length > 0 &&
<HeaderFields title={$t('响应 Header')} rows={ responseList?.[0]?.responseParams?.headerParams}
onMoreSettingChange={setCurrentMoreSettingParam} />
}
{responseList?.[0]?.responseParams?.headerParams?.length > 0 && (
<HeaderFields
title={$t('响应 Header')}
rows={responseList?.[0]?.responseParams?.headerParams}
onMoreSettingChange={setCurrentMoreSettingParam}
/>
)}
{responseBodyList?.length > 0 &&
<MessageBodyComponent
title={$t("响应 Body")}
rows={responseBodyList}
contentType={responseList?.[0]?.contentType}
onMoreSettingChange={setCurrentMoreSettingParam}
/>
}
{responseBodyList?.length > 0 && (
<MessageBodyComponent
title={$t('响应 Body')}
rows={responseBodyList}
contentType={responseList?.[0]?.contentType}
onMoreSettingChange={setCurrentMoreSettingParam}
/>
)}
<MoreSetting
readOnly={true}
open={Boolean(currentMoreSettingParam)}
onClose={handleCloseMoreSetting}
hiddenConfig={moreSettingHiddenConfig}
param={currentMoreSettingParam as unknown as RenderMessageBody}
readOnly={true}
open={Boolean(currentMoreSettingParam)}
onClose={handleCloseMoreSetting}
hiddenConfig={moreSettingHiddenConfig}
param={currentMoreSettingParam as unknown as RenderMessageBody}
/>
</ThemeProvider>
</>)
</ThemeProvider>
</>
)
}
@@ -1,287 +1,289 @@
import { Box, Button, LinearProgress, Stack } from '@mui/material'
import { Box, LinearProgress, Stack } from '@mui/material'
import { Allotment } from 'allotment'
import { memo, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { useAutoAnimate } from '@formkit/auto-animate/react'
import {HTTPMethod} from "./api/RequestMethod";
import { HTTPMethod } from './api/RequestMethod'
import {
ApiBodyType,
ApiDetail,
BodyParamsType,
HeaderParamsType,
QueryParamsType, ResponseList, RestParamsType,
TestApiBodyType
} from "@common/const/api-detail";
import {extractBraceContent, mapContentTypeToApiBodyType, syncUrlAndQuery} from "@common/utils/postcat.tsx";
import {generateRow} from "./api/ApiManager/components/MessageDataGrid/constants.ts";
import {RawParams} from "./api/ApiManager/components/ApiMessageBody";
import {ParseCurlResult} from "@common/utils/curl.ts";
import {ApiRequestTester, ApiRequestTesterApi} from "./api/ApiTest/components/ApiRequestTester";
import {TestResponse} from "@common/hooks/useTest.ts";
import {getDefaultApiInfo} from "./api/ApiManager/constants.ts";
import {UriInput} from "./api/ApiManager/components/UriInput";
import {TestControl} from "./api/ApiTest/components/TestControl";
ApiBodyType,
ApiDetail,
BodyParamsType,
HeaderParamsType,
QueryParamsType,
ResponseList,
RestParamsType,
TestApiBodyType
} from '@common/const/api-detail'
import { extractBraceContent, mapContentTypeToApiBodyType, syncUrlAndQuery } from '@common/utils/postcat.tsx'
import { generateRow } from './api/ApiManager/components/MessageDataGrid/constants.ts'
import { RawParams } from './api/ApiManager/components/ApiMessageBody'
import { ParseCurlResult } from '@common/utils/curl.ts'
import { ApiRequestTester, ApiRequestTesterApi } from './api/ApiTest/components/ApiRequestTester'
import { TestResponse } from '@common/hooks/useTest.ts'
import { getDefaultApiInfo } from './api/ApiManager/constants.ts'
import { UriInput } from './api/ApiManager/components/UriInput'
import { TestControl } from './api/ApiTest/components/TestControl'
const Tester = memo(ApiRequestTester)
import 'allotment/dist/style.css'
import {ApiResponse} from "./api/ApiTest/components/ApiResponse";
import { ApiResponse } from './api/ApiTest/components/ApiResponse'
type SafeAny = unknown
export interface ApiTestApiRef {
getTestMeta: () => SafeAny
getTestMeta: () => SafeAny
}
export default function ApiTest({ apiRef, apiInfo,loaded = true}: { apiRef?: React.RefObject<ApiTestApiRef> ,apiInfo:ApiDetail,loaded?:boolean}) {
const [uri, setUri] = useState<string>('')
const [httpMethod, setHttpMethod] = useState<HTTPMethod>(HTTPMethod.POST)
const [testResponse, setTestResponse] = useState<TestResponse | null>(null)
// const { apiInfo, loaded } = useContext<Partial<ApiTabContextProps>>(ApiTabContext)
const testerApiRef = useRef<ApiRequestTesterApi>(null)
const [parent] = useAutoAnimate()
// const testApiInfo:ApiDetail = apiInfo
const [isLoading, setIsLoading] = useState<boolean>()
const [cancel,setCancel] = useState<boolean>()
export default function ApiTest({
apiRef,
apiInfo,
loaded = true
}: {
apiRef?: React.RefObject<ApiTestApiRef>
apiInfo: ApiDetail
loaded?: boolean
}) {
const [uri, setUri] = useState<string>('')
const [httpMethod, setHttpMethod] = useState<HTTPMethod>(HTTPMethod.POST)
const [testResponse, setTestResponse] = useState<TestResponse | null>(null)
// const { apiInfo, loaded } = useContext<Partial<ApiTabContextProps>>(ApiTabContext)
const testerApiRef = useRef<ApiRequestTesterApi>(null)
const [parent] = useAutoAnimate()
// const testApiInfo:ApiDetail = apiInfo
const [isLoading, setIsLoading] = useState<boolean>()
const [cancel, setCancel] = useState<boolean>()
// useEffect(() => {
// if (testApiInfo) {
// const data: SafeAny = testApiInfo
// const responseResult = {
// report: {
// general: {
// downloadRate: data.downloadRate,
// downloadSize: data.downloadSize,
// redirectTimes: data.redirectTimes,
// time: data.time,
// timingSummary: data.timingSummary
// },
// request: {
// headers: testApiInfo.requestParams.headerParams?.map((item) => ({
// value: item.paramAttr.example,
// key: item.name
// })),
// requestType: data.request.contentType,
// body: testApiInfo.requestParams.bodyParams,
// uri: testApiInfo.uri
// },
// response: {
// headers: data.headers.map((item: SafeAny) => ({ key: item.name, value: item.value })),
// body: data.body,
// contentType: data.contentType,
// httpCode: data.statusCode,
// responseType: data.responseType
// }
// }
// } as unknown as TestResponse
// setTestResponse(responseResult)
// setHttpMethod(testApiInfo.method)
// testerApiRef.current?.updateHeaderDataGrid((testApiInfo.requestParams.headerParams as HeaderParamsType[]) || [])
// const apiBodyType: TestApiBodyType = testApiInfo.requestParams.bodyParams[0]?.contentType as TestApiBodyType
// const contentType = apiBodyType === ApiBodyType.Raw ? 'application/json' : 'application/x-www-form-urlencoded'
// testerApiRef.current?.updateRequestBody({
// apiBodyType,
// contentType: contentType,
// data:
// apiBodyType === ApiBodyType.Raw
// ? testApiInfo.requestParams.bodyParams?.[0]?.binaryRawData
// : testApiInfo.requestParams.bodyParams
// })
// setTimeout(() => {
// setUri(testApiInfo.uri)
// testerApiRef.current?.updateQueryDataGrid([])
// testerApiRef.current?.updateRestDataGrid([])
// }, 0)
// // updateTestApiInfo(null)
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [testApiInfo])
useEffect(() => {
if (apiInfo && loaded) {
setUri(apiInfo.uri)
setHttpMethod(apiInfo.method)
testerApiRef.current?.updateHeaderDataGrid((apiInfo.requestParams.headerParams as HeaderParamsType[]) || [])
const apiBodyType: TestApiBodyType = apiInfo.requestParams?.bodyParams[0]?.contentType as TestApiBodyType
const contentType = apiBodyType === ApiBodyType.Raw ? 'application/json' : 'application/x-www-form-urlencoded'
testerApiRef.current?.updateRequestBody({
apiBodyType,
contentType: contentType,
data:
apiBodyType === ApiBodyType.Raw
? apiInfo.requestParams?.bodyParams?.[0]?.binaryRawData
: apiInfo.requestParams?.bodyParams
})
}
}, [apiInfo, loaded])
// useEffect(() => {
// if (testApiInfo) {
// const data: SafeAny = testApiInfo
// const responseResult = {
// report: {
// general: {
// downloadRate: data.downloadRate,
// downloadSize: data.downloadSize,
// redirectTimes: data.redirectTimes,
// time: data.time,
// timingSummary: data.timingSummary
// },
// request: {
// headers: testApiInfo.requestParams.headerParams?.map((item) => ({
// value: item.paramAttr.example,
// key: item.name
// })),
// requestType: data.request.contentType,
// body: testApiInfo.requestParams.bodyParams,
// uri: testApiInfo.uri
// },
// response: {
// headers: data.headers.map((item: SafeAny) => ({ key: item.name, value: item.value })),
// body: data.body,
// contentType: data.contentType,
// httpCode: data.statusCode,
// responseType: data.responseType
// }
// }
// } as unknown as TestResponse
// setTestResponse(responseResult)
// setHttpMethod(testApiInfo.method)
// testerApiRef.current?.updateHeaderDataGrid((testApiInfo.requestParams.headerParams as HeaderParamsType[]) || [])
// const apiBodyType: TestApiBodyType = testApiInfo.requestParams.bodyParams[0]?.contentType as TestApiBodyType
// const contentType = apiBodyType === ApiBodyType.Raw ? 'application/json' : 'application/x-www-form-urlencoded'
// testerApiRef.current?.updateRequestBody({
// apiBodyType,
// contentType: contentType,
// data:
// apiBodyType === ApiBodyType.Raw
// ? testApiInfo.requestParams.bodyParams?.[0]?.binaryRawData
// : testApiInfo.requestParams.bodyParams
// })
// setTimeout(() => {
// setUri(testApiInfo.uri)
// testerApiRef.current?.updateQueryDataGrid([])
// testerApiRef.current?.updateRestDataGrid([])
// }, 0)
// // updateTestApiInfo(null)
useImperativeHandle(apiRef, () => ({
getTestMeta: () => {
const { rest, query, headers, body } = testerApiRef.current?.getEditMeta() || {}
return {
uri,
restParams: rest || [],
headersParams: (headers as HeaderParamsType[]) || [],
bodyParams: (body?.data as BodyParamsType[]) || [],
queryParams: query || [],
method: httpMethod,
requestType: body!.apiBodyType
}
}
}))
const handleTest = async () => {
const { rest, headers, body } = testerApiRef.current?.getEditMeta() || {}
// const response = await test(
// { apiId, workspaceId, projectId },
// {
// uri,
// restParams: rest || [],
// headersParams: (headers as HeaderParamsType[]) || [],
// bodyParams: (body?.data as BodyParamsType[]) || [],
// method: httpMethod,
// requestType: body!.apiBodyType
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [testApiInfo])
// )
// const response = {data:{}}
// if (response.data?.report?.request) {
// response.data.report.request.uri = uri
// }
// setTestResponse(response.data)
}
useEffect(() => {
if (apiInfo && loaded) {
setUri(apiInfo.uri)
setHttpMethod(apiInfo.method)
testerApiRef.current?.updateHeaderDataGrid((apiInfo.requestParams.headerParams as HeaderParamsType[]) || [])
const apiBodyType: TestApiBodyType = apiInfo.requestParams?.bodyParams[0]?.contentType as TestApiBodyType
const contentType = apiBodyType === ApiBodyType.Raw ? 'application/json' : 'application/x-www-form-urlencoded'
testerApiRef.current?.updateRequestBody({
apiBodyType,
contentType: contentType,
data:
apiBodyType === ApiBodyType.Raw
? apiInfo.requestParams?.bodyParams?.[0]?.binaryRawData
: apiInfo.requestParams?.bodyParams
})
}
}, [apiInfo, loaded])
useImperativeHandle(apiRef, () => ({
getTestMeta: () => {
const {
rest,
query,
headers,
body,
} = testerApiRef.current?.getEditMeta() || {}
return {
uri,
restParams: rest || [],
headersParams: (headers as HeaderParamsType[]) || [],
bodyParams: (body?.data as BodyParamsType[]) || [],
queryParams: query || [],
method: httpMethod,
requestType: body!.apiBodyType
}
}
}))
const handleTest = async () => {
const { rest, headers, body} = testerApiRef.current?.getEditMeta() || {}
// const response = await test(
// { apiId, workspaceId, projectId },
// {
// uri,
// restParams: rest || [],
// headersParams: (headers as HeaderParamsType[]) || [],
// bodyParams: (body?.data as BodyParamsType[]) || [],
// method: httpMethod,
// requestType: body!.apiBodyType
// }
// )
// const response = {data:{}}
// if (response.data?.report?.request) {
// response.data.report.request.uri = uri
// }
// setTestResponse(response.data)
const handleQueryChange = useCallback((queryList: QueryParamsType[]) => {
/** Can't use new URL due to potential non-standard URLs; reverting to pre-refactor code temporarily. */
const result = syncUrlAndQuery(uri, queryList as SafeAny, {
nowOperate: 'query',
method: 'replace'
})
if (result?.url) {
setUri(result.url)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleQueryChange = useCallback((queryList: QueryParamsType[]) => {
/** Can't use new URL due to potential non-standard URLs; reverting to pre-refactor code temporarily. */
const result = syncUrlAndQuery(uri, queryList as SafeAny, {
nowOperate: 'query',
method: 'replace'
const handleUriChange = (uri: string) => {
setUri(uri)
// if (activeTab?.path === quickTestRoute.path) {
// updateTab({ ...activeTab, name: uri || 'New Request', method: httpMethod } as TabRouteObject)
// }
if (uri) {
const restResult = extractBraceContent(uri)
const queryResult = syncUrlAndQuery(uri, [])
queryResult?.query?.length && testerApiRef.current?.updateQueryDataGrid(queryResult.query)
restResult?.length &&
testerApiRef.current?.updateRestDataGrid(restResult.map((rest) => ({ name: rest })) as RestParamsType[])
}
}
const handleHttpMethodChange = (method: HTTPMethod) => {
setHttpMethod(method)
// if (activeTab?.path === quickTestRoute.path) {
// updateTab({ ...activeTab, name: uri || 'New Request', method } as TabRouteObject)
// }
}
/** Execute this logic only during 'Quicktest' run. */
const handleSaveApi = () => {
const { rest, query, headers, body, preScript = '', postScript = '' } = testerApiRef.current?.getEditMeta() || {}
const newApiInfo = getDefaultApiInfo()
const contentType = mapContentTypeToApiBodyType(body?.contentType ?? 'text/plain')
newApiInfo.uri = uri
newApiInfo.name = 'New Request'
newApiInfo.apiAttrInfo.requestMethod = httpMethod
newApiInfo.apiAttrInfo.contentType = contentType
newApiInfo.requestParams = {
bodyParams: body?.data,
headerParams: headers,
queryParams: query,
restParams: rest
} as SafeAny
if (testResponse) {
const response = testResponse.report.response
const responseHeader = response?.headers.map((header) => {
return generateRow({
name: header.key,
paramAttr: {
example: header.value
}
})
if (result?.url) {
setUri(result.url)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleUriChange = (uri: string) => {
setUri(uri)
// if (activeTab?.path === quickTestRoute.path) {
// updateTab({ ...activeTab, name: uri || 'New Request', method: httpMethod } as TabRouteObject)
// }
if (uri) {
const restResult = extractBraceContent(uri)
const queryResult = syncUrlAndQuery(uri, [])
queryResult?.query?.length && testerApiRef.current?.updateQueryDataGrid(queryResult.query)
restResult?.length &&
testerApiRef.current?.updateRestDataGrid(restResult.map((rest) => ({ name: rest })) as RestParamsType[])
})
const responseList = [
{
// TODO: response JSON?
contentType: ApiBodyType.Raw,
responseParams: {
bodyParams: [RawParams(response?.body || '')],
headerParams: responseHeader || [],
queryParams: [],
restParams: []
}
}
]
newApiInfo.responseList = responseList as unknown as ResponseList[]
}
}
const handleHttpMethodChange = (method: HTTPMethod) => {
setHttpMethod(method)
// if (activeTab?.path === quickTestRoute.path) {
// updateTab({ ...activeTab, name: uri || 'New Request', method } as TabRouteObject)
// }
}
const handleCURLParse = (cURLResult: ParseCurlResult) => {
setHttpMethod(HTTPMethod[cURLResult.method as keyof typeof HTTPMethod])
/** cURLResult.body */
const headers = Object.keys(cURLResult.headers).map((key) => ({
name: key,
paramAttr: {
example: cURLResult.headers[key]
}
}))
testerApiRef.current?.updateHeaderDataGrid((headers as HeaderParamsType[]) || [])
testerApiRef.current?.updateRequestBodyWithCurlInfo(cURLResult)
setTimeout(() => {
setUri(cURLResult.url)
testerApiRef.current?.updateQueryDataGrid([])
testerApiRef.current?.updateRestDataGrid([])
}, 0)
}
/** Execute this logic only during 'Quicktest' run. */
const handleSaveApi = () => {
const { rest, query, headers, body, preScript = '', postScript = '' } = testerApiRef.current?.getEditMeta() || {}
const newApiInfo = getDefaultApiInfo()
const contentType = mapContentTypeToApiBodyType(body?.contentType ?? 'text/plain')
newApiInfo.uri = uri
newApiInfo.name = 'New Request'
newApiInfo.apiAttrInfo.requestMethod = httpMethod
newApiInfo.apiAttrInfo.contentType = contentType
newApiInfo.requestParams = {
bodyParams: body?.data,
headerParams: headers,
queryParams: query,
restParams: rest
} as SafeAny
if (testResponse) {
const response = testResponse.report.response
const responseHeader = response?.headers.map((header) => {
return generateRow({
name: header.key,
paramAttr: {
example: header.value
}
})
})
const responseList = [
{
// TODO: response JSON?
contentType: ApiBodyType.Raw,
responseParams: {
bodyParams: [RawParams(response?.body || '')],
headerParams: responseHeader || [],
queryParams: [],
restParams: []
}
}
]
newApiInfo.responseList = responseList as unknown as ResponseList[]
}
}
const handleCURLParse = (cURLResult: ParseCurlResult) => {
setHttpMethod(HTTPMethod[cURLResult.method as keyof typeof HTTPMethod])
/** cURLResult.body */
const headers = Object.keys(cURLResult.headers).map((key) => ({
name: key,
paramAttr: {
example: cURLResult.headers[key]
}
}))
testerApiRef.current?.updateHeaderDataGrid((headers as HeaderParamsType[]) || [])
testerApiRef.current?.updateRequestBodyWithCurlInfo(cURLResult)
setTimeout(() => {
setUri(cURLResult.url)
testerApiRef.current?.updateQueryDataGrid([])
testerApiRef.current?.updateRestDataGrid([])
}, 0)
}
return (
<Box height="100%" width="calc(100% - 300px)" boxSizing="border-box">
<Stack direction="column" spacing={1} height="100%">
<Box height="100%" width="100%">
<Allotment vertical>
<Allotment.Pane snap>
<Box p={2} height="100%" display="flex" boxSizing="border-box" flexDirection="column">
<Box height="40px">
<Box display="flex" gap={2}>
<UriInput
inputValue={uri}
onInputChange={handleUriChange}
selectValue={httpMethod}
onSelectChange={handleHttpMethodChange}
onCURLPaste={handleCURLParse}
onTest={handleTest}
/>
<Box flexShrink={0} display="flex" gap={2}>
<TestControl loading={isLoading} onTest={handleTest} onAbort={cancel} />
</Box>
</Box>
</Box>
<Box height="calc(100% - 40px)">
<Tester apiRef={testerApiRef} apiInfo={apiInfo} onQueryChange={handleQueryChange} />
</Box>
</Box>
</Allotment.Pane>
<Allotment.Pane preferredSize={300} snap minSize={180}>
<Box ref={parent} height="100%" width="100%">
{isLoading ? <LinearProgress /> : null}
<ApiResponse data={testResponse} />
</Box>
</Allotment.Pane>
</Allotment>
return (
<Box height="100%" width="calc(100% - 300px)" boxSizing="border-box">
<Stack direction="column" spacing={1} height="100%">
<Box height="100%" width="100%">
<Allotment vertical>
<Allotment.Pane snap>
<Box p={2} height="100%" display="flex" boxSizing="border-box" flexDirection="column">
<Box height="40px">
<Box display="flex" gap={2}>
<UriInput
inputValue={uri}
onInputChange={handleUriChange}
selectValue={httpMethod}
onSelectChange={handleHttpMethodChange}
onCURLPaste={handleCURLParse}
onTest={handleTest}
/>
<Box flexShrink={0} display="flex" gap={2}>
<TestControl loading={isLoading} onTest={handleTest} onAbort={cancel} />
</Box>
</Box>
</Box>
</Stack>
<Box height="calc(100% - 40px)">
<Tester apiRef={testerApiRef} apiInfo={apiInfo} onQueryChange={handleQueryChange} />
</Box>
</Box>
</Allotment.Pane>
<Allotment.Pane preferredSize={300} snap minSize={180}>
<Box ref={parent} height="100%" width="100%">
{isLoading ? <LinearProgress /> : null}
<ApiResponse data={testResponse} />
</Box>
</Allotment.Pane>
</Allotment>
</Box>
)
</Stack>
</Box>
)
}
@@ -1,11 +1,11 @@
import { $t } from '@common/locales';
import { $t } from '@common/locales'
import { TextField } from '@mui/material'
import { SyntheticEvent } from 'react'
export function RequestBodyBinary({ value, onChange }: { value: string; onChange: (value: string) => void }) {
return (
<TextField
label={$t("Binary")}
label={$t('Binary')}
multiline
rows={4}
value={value}
@@ -1,4 +1,4 @@
import {Codebox} from "../../../../Codebox";
import { Codebox } from '../../../../Codebox'
interface RequestBodyRawProps {
value: string
@@ -1,5 +1,4 @@
import { ApiBodyType, ApiParamsType } from "@common/const/api-detail"
import { ApiBodyType, ApiParamsType } from '@common/const/api-detail'
export type ApiBodyTypeLabel = 'Form-Data' | 'JSON' | 'XML' | 'Raw' | 'Binary'
@@ -1,11 +1,20 @@
import {Box, FormControl, FormControlLabel, MenuItem, Radio, RadioGroup, Select, SelectChangeEvent} from '@mui/material'
import {ApiBodyTypeOption} from './constants'
import {ChangeEvent, SetStateAction, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'
import {RequestBodyRaw} from './components/Raw'
import {RequestBodyBinary} from './components/Binary'
import {ApiBodyType, BodyParamsType} from "@common/const/api-detail";
import {generateId} from "@common/utils/postcat.tsx";
import {MessageDataGrid, MessageDataGridApi} from "../MessageDataGrid";
import {
Box,
FormControl,
FormControlLabel,
MenuItem,
Radio,
RadioGroup,
Select,
SelectChangeEvent
} from '@mui/material'
import { ApiBodyTypeOption } from './constants'
import { ChangeEvent, SetStateAction, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { RequestBodyRaw } from './components/Raw'
import { RequestBodyBinary } from './components/Binary'
import { ApiBodyType, BodyParamsType } from '@common/const/api-detail'
import { generateId } from '@common/utils/postcat.tsx'
import { MessageDataGrid, MessageDataGridApi } from '../MessageDataGrid'
export interface ApiMessageBodyApi {
getBodyMeta: () => {
@@ -43,7 +52,7 @@ export function RawParams(value: string) {
}
}
export function ApiMessageBody({ apiInfo=null, loaded, mode, bodyApiRef }: ApiBodyParamsTypeProps) {
export function ApiMessageBody({ apiInfo = null, loaded, mode, bodyApiRef }: ApiBodyParamsTypeProps) {
const [apiBodyTypeValue, setApiBodyTypeValue] = useState<ApiBodyType>(ApiBodyType.JSON)
const [apiFormData, setApiFormData] = useState<BodyParamsType[] | null>([])
@@ -67,7 +76,9 @@ export function ApiMessageBody({ apiInfo=null, loaded, mode, bodyApiRef }: ApiBo
}[apiBodyTypeValue as ApiBodyType.JSON | ApiBodyType.FormData | ApiBodyType.XML]
bodyParams.push(...(targetRef.current?.getEditMeta() as BodyParamsType[]))
} else if ([ApiBodyType.Raw, ApiBodyType.Binary].includes(apiBodyTypeValue)) {
bodyParams.push(RawParams(apiBodyTypeValue === ApiBodyType.Raw ? apiRaw : apiBinary) as unknown as BodyParamsType)
bodyParams.push(
RawParams(apiBodyTypeValue === ApiBodyType.Raw ? apiRaw : apiBinary) as unknown as BodyParamsType
)
}
return {
@@ -86,7 +97,7 @@ export function ApiMessageBody({ apiInfo=null, loaded, mode, bodyApiRef }: ApiBo
// }
// }
// },500)
}, []);
}, [])
useEffect(() => {
if (loaded && (apiInfo || apiInfo === null)) {
@@ -193,14 +204,12 @@ export function ApiMessageBody({ apiInfo=null, loaded, mode, bodyApiRef }: ApiBo
{
key: 'Raw',
value: ApiBodyType.Raw,
element: <RequestBodyRaw value={apiRaw} onChange={setApiRaw}
loaded={loaded} />
element: <RequestBodyRaw value={apiRaw} onChange={setApiRaw} loaded={loaded} />
},
{
key: 'Binary',
value: ApiBodyType.Binary,
element: <RequestBodyBinary value={apiBinary} onChange={setApiBinary}
loaded={loaded}/>
element: <RequestBodyBinary value={apiBinary} onChange={setApiBinary} loaded={loaded} />
}
].filter((type) => type) as ApiBodyTypeOption[]
}, [apiBinary, apiFormData, apiJson, apiRaw, apiXml, mode])
@@ -214,23 +223,23 @@ export function ApiMessageBody({ apiInfo=null, loaded, mode, bodyApiRef }: ApiBo
}
return (
<Box >
<Box sx={{display:'flex',alignItems:'CENTER',paddingY:'4px'}}>
<Box>
<Box sx={{ display: 'flex', alignItems: 'CENTER', paddingY: '4px' }}>
<FormControl>
<RadioGroup
row
name="api-body-type-radio-buttons-group"
value={apiBodyTypeValue}
onChange={handleApiBodyTypeValueChange}
sx={{ height: '30px' ,paddingLeft:'8px',marginLeft:'8px',fontSize:'12px'}}
sx={{ height: '30px', paddingLeft: '8px', marginLeft: '8px', fontSize: '12px' }}
>
{apiBodyTypeList.map((apiBodyType) => (
<FormControlLabel
sx={{ paddingRight:'10px' ,'& .MuiFormControlLabel-label': { fontSize: '12px' }, }}
sx={{ paddingRight: '10px', '& .MuiFormControlLabel-label': { fontSize: '12px' } }}
key={apiBodyType.value}
value={apiBodyType.value}
checked={apiBodyType.value === apiBodyTypeValue}
control={<Radio sx={{ height: '30px',width:'22px',marginRight:'4px',color:'#EDEDED' }} />}
control={<Radio sx={{ height: '30px', width: '22px', marginRight: '4px', color: '#EDEDED' }} />}
label={apiBodyType.key}
/>
))}
@@ -243,8 +252,7 @@ export function ApiMessageBody({ apiInfo=null, loaded, mode, bodyApiRef }: ApiBo
onChange={handleJsonTypeChange}
sx={{
height: '30px',
borderColor:'#EDEDED'
borderColor: '#EDEDED'
}}
>
<MenuItem value={ApiBodyType.JSON}>Object</MenuItem>
@@ -1,20 +1,19 @@
import { Box, useTheme } from '@mui/material'
import {ApiBodyType, BodyParamsType} from "@common/const/api-detail";
import {RenderMessageBody} from "@common/components/postcat/api/apiManager/components/MessageDataGrid";
import { BodyParamsType } from '@common/const/api-detail'
import { RenderMessageBody } from '@common/components/postcat/api/apiManager/components/MessageDataGrid'
export interface ApiProxyEditorApi {
getEditMeta: () => Partial<BodyParamsType>[]
}
interface ApiProxyEditorProps<T = unknown> {
// onChange?: (rows: T[]) => void
// initialRows?: T[] | null
// onDirty?: () => void
// onChange?: (rows: T[]) => void
// initialRows?: T[] | null
// onDirty?: () => void
loading?: boolean
// messageType?: MessageType
// contentType: ContentType
// isMoreSettingReadOnly?: boolean
// apiRef?: RefObject<ApiProxyEditorApi>
// messageType?: MessageType
// contentType: ContentType
// isMoreSettingReadOnly?: boolean
// apiRef?: RefObject<ApiProxyEditorApi>
}
export function ApiProxyEditor(props: ApiProxyEditorProps<RenderMessageBody>) {
@@ -22,7 +21,7 @@ export function ApiProxyEditor(props: ApiProxyEditorProps<RenderMessageBody>) {
// onChange,
// initialRows,
// onDirty,
loading = false,
loading = false
// contentType,
// messageType,
// isMoreSettingReadOnly,
@@ -31,7 +30,6 @@ export function ApiProxyEditor(props: ApiProxyEditorProps<RenderMessageBody>) {
} = props
const theme = useTheme()
return (
<Box
sx={{
@@ -39,10 +37,7 @@ export function ApiProxyEditor(props: ApiProxyEditorProps<RenderMessageBody>) {
borderRadius: `${theme.shape.borderRadius}px`
}}
>
<Box>
</Box>
<Box></Box>
</Box>
)
}
@@ -1,16 +1,17 @@
import { Box, Grow, Tab, Tabs, Typography, useTheme } from '@mui/material'
import {ReactNode, SyntheticEvent, useEffect, useImperativeHandle, useRef, useState} from 'react'
import { ReactNode, SyntheticEvent, useEffect, useImperativeHandle, useRef, useState } from 'react'
import {
ApiBodyType, ApiDetail,
ApiBodyType,
ApiDetail,
BodyParamsType,
HeaderParamsType,
QueryParamsType,
RestParamsType
} from "@common/const/api-detail";
import {MessageDataGrid, MessageDataGridApi} from "../MessageDataGrid";
import {Indicator} from "../../../../Indicator";
import { ApiMessageBody, ApiMessageBodyApi } from '../ApiMessageBody';
import { $t } from '@common/locales';
} from '@common/const/api-detail'
import { MessageDataGrid, MessageDataGridApi } from '../MessageDataGrid'
import { Indicator } from '../../../../Indicator'
import { ApiMessageBody, ApiMessageBodyApi } from '../ApiMessageBody'
import { $t } from '@common/locales'
export interface ApiRequestEditorApi {
getData: () => {
@@ -30,21 +31,31 @@ interface ApiRequestEditorTab {
dirty: boolean
}
export function ApiRequestEditor({ editorRef ,apiInfo=null,loaded}: { editorRef?: React.RefObject<ApiRequestEditorApi> ,apiInfo:ApiDetail,loaded:boolean}) {
const [apiHeaders, setApiHeaders] = useState<HeaderParamsType[] >([])
const [apiQuery, setApiQuery] = useState<QueryParamsType[] >([])
const [apiRest, setApiRest] = useState<RestParamsType[] >([])
export function ApiRequestEditor({
editorRef,
apiInfo = null,
loaded
}: {
editorRef?: React.RefObject<ApiRequestEditorApi>
apiInfo: ApiDetail
loaded: boolean
}) {
const [apiHeaders, setApiHeaders] = useState<HeaderParamsType[]>([])
const [apiQuery, setApiQuery] = useState<QueryParamsType[]>([])
const [apiRest, setApiRest] = useState<RestParamsType[]>([])
const headersRef = useRef<MessageDataGridApi>(null)
const bodyRef = useRef<ApiMessageBodyApi>(null)
const queryRef = useRef<MessageDataGridApi>(null)
const restRef = useRef<MessageDataGridApi>(null)
const [innerLoaded,setInnerLoaded] = useState<boolean>(false)
const [innerLoaded, setInnerLoaded] = useState<boolean>(false)
useImperativeHandle(editorRef, () => ({
getData: () => {
return {
bodyParams: bodyRef.current?.getBodyMeta()?.bodyParams.map((x)=>({...x,contentType:bodyRef.current?.getBodyMeta().contentType})),
bodyParams: bodyRef.current
?.getBodyMeta()
?.bodyParams.map((x) => ({ ...x, contentType: bodyRef.current?.getBodyMeta().contentType })),
headerParams: (headersRef.current?.getEditMeta() as HeaderParamsType[]) || [],
queryParams: (queryRef.current?.getEditMeta() as QueryParamsType[]) || [],
restParams: (restRef.current?.getEditMeta() as RestParamsType[]) || []
@@ -77,8 +88,8 @@ export function ApiRequestEditor({ editorRef ,apiInfo=null,loaded}: { editorRef?
dirty: false
},
{
label:$t('请求体'),
element: <ApiMessageBody bodyApiRef={bodyRef} mode="request" apiInfo={apiInfo} loaded={innerLoaded}/>,
label: $t('请求体'),
element: <ApiMessageBody bodyApiRef={bodyRef} mode="request" apiInfo={apiInfo} loaded={innerLoaded} />,
dirty: false
},
{
@@ -123,12 +134,15 @@ export function ApiRequestEditor({ editorRef ,apiInfo=null,loaded}: { editorRef?
const tabHeight = '30px'
return (
<Box sx={{
borderColor: 'divider' }}>
<Box
sx={{
borderColor: 'divider'
}}
>
<Tabs
value={tabValue}
onChange={handleChange}
aria-label={$t("api request editor")}
aria-label={$t('api request editor')}
sx={{
minHeight: tabHeight,
height: tabHeight,
@@ -144,7 +158,7 @@ export function ApiRequestEditor({ editorRef ,apiInfo=null,loaded}: { editorRef?
value={tab.label}
label={
<Box key={tab.label} display="flex" alignItems="center" pr={tab.dirty ? 0.5 : 0}>
<Typography sx={{fontSize:'14px'}}>{tab.label}</Typography>
<Typography sx={{ fontSize: '14px' }}>{tab.label}</Typography>
<Grow in={tab.dirty}>
<Box>
<Indicator
@@ -1,11 +1,11 @@
import { Box, Grow, Tab, Tabs, Typography, useTheme } from '@mui/material'
import { ReactNode, SyntheticEvent, useContext, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { ReactNode, SyntheticEvent, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { MessageDataGrid, MessageDataGridApi } from '../MessageDataGrid'
import {ApiBodyType, BodyParamsType, HeaderParamsType} from "@common/const/api-detail";
import {Indicator} from "../../../../Indicator";
import { v4 as uuidv4} from 'uuid'
import { ApiMessageBody, ApiMessageBodyApi } from '../ApiMessageBody';
import { $t } from '@common/locales';
import { ApiBodyType, BodyParamsType, HeaderParamsType } from '@common/const/api-detail'
import { Indicator } from '../../../../Indicator'
import { v4 as uuidv4 } from 'uuid'
import { ApiMessageBody, ApiMessageBodyApi } from '../ApiMessageBody'
import { $t } from '@common/locales'
interface ApiRequestEditorTab {
label: string
@@ -23,7 +23,13 @@ export interface ApiResponseEditorApi {
}
}
export function ApiResponseEditor({ editorRef ,apiInfo=null, loaded}: { editorRef?: React.RefObject<ApiResponseEditorApi> }) {
export function ApiResponseEditor({
editorRef,
apiInfo = null,
loaded
}: {
editorRef?: React.RefObject<ApiResponseEditorApi>
}) {
const [apiHeaders, setApiHeaders] = useState<HeaderParamsType[] | null>([])
const [innerLoaded, setInnerLoaded] = useState<boolean>(false)
const headersRef = useRef<MessageDataGridApi>(null)
@@ -40,16 +46,17 @@ export function ApiResponseEditor({ editorRef ,apiInfo=null, loaded}: { editorRe
getData: () => {
const bodyData = bodyRef.current?.getBodyMeta()
const uuid = uuidv4()
return ([{
id:uuid,
responseUuid:uuid,
httpCode:bodyData?.contentType,
responseParams:{
bodyParams: bodyData?.bodyParams,
headerParams: (headersRef.current?.getEditMeta() as HeaderParamsType[]) || []
return [
{
id: uuid,
responseUuid: uuid,
httpCode: bodyData?.contentType,
responseParams: {
bodyParams: bodyData?.bodyParams,
headerParams: (headersRef.current?.getEditMeta() as HeaderParamsType[]) || []
}
}
}
])
]
}
}))
@@ -70,8 +77,7 @@ export function ApiResponseEditor({ editorRef ,apiInfo=null, loaded}: { editorRe
},
{
label: $t('返回值'),
element: <ApiMessageBody bodyApiRef={bodyRef} mode="response" apiInfo={apiInfo}
loaded={innerLoaded} />,
element: <ApiMessageBody bodyApiRef={bodyRef} mode="response" apiInfo={apiInfo} loaded={innerLoaded} />,
dirty: false
}
]
@@ -88,17 +94,21 @@ export function ApiResponseEditor({ editorRef ,apiInfo=null, loaded}: { editorRe
const tabHeight = '30px'
return (
<Box sx={{
// borderBottom: 1,
borderColor: 'divider' }}>
<Box
sx={{
// borderBottom: 1,
borderColor: 'divider'
}}
>
<Tabs
value={tabValue}
onChange={handleChange}
aria-label={$t("api request editor")}
aria-label={$t('api request editor')}
sx={{
minHeight: tabHeight,
height: tabHeight,
borderBottom: 1, borderColor: 'divider',
borderBottom: 1,
borderColor: 'divider',
'& .MuiTabs-flexContainer': {
minHeight: tabHeight,
height: tabHeight
@@ -111,7 +121,7 @@ export function ApiResponseEditor({ editorRef ,apiInfo=null, loaded}: { editorRe
value={tab.label}
label={
<Box key={tab.label} display="flex" alignItems="center" pr={tab.dirty ? 0.5 : 0}>
<Typography sx={{fontSize:'14px'}}>{tab.label}</Typography>
<Typography sx={{ fontSize: '14px' }}>{tab.label}</Typography>
<Grow in={tab.dirty}>
<Box>
<Indicator
@@ -1,4 +1,3 @@
import { Box, SxProps, TextFieldProps, Theme } from '@mui/material'
import { HTMLAttributes, KeyboardEvent } from 'react'
@@ -1,17 +1,19 @@
import {generateId} from "@common/utils/postcat.tsx";
import { generateId } from '@common/utils/postcat.tsx'
type SafeAny = unknown
export function generateRow(data: SafeAny = {}) {
return Object.assign({
id: generateId(),
name: '',
dataType: null,
isRequired: 1,
description: '',
paramAttr: {
example: ''
return Object.assign(
{
id: generateId(),
name: '',
dataType: null,
isRequired: 1,
description: '',
paramAttr: {
example: ''
},
childList: []
},
childList: []
}, data)
}
data
)
}
@@ -1,6 +1,6 @@
import {MessageType, RenderMessageBody} from "../index.tsx";
import {isNil} from "@common/utils/postcat.tsx";
import {ApiParamsType} from "@common/const/api-detail";
import { MessageType, RenderMessageBody } from '../index.tsx'
import { isNil } from '@common/utils/postcat.tsx'
import { ApiParamsType } from '@common/const/api-detail'
interface UseMoreSettingHiddenConfigProps {
param: RenderMessageBody
@@ -22,26 +22,26 @@ import {
} from 'react'
import { ApiParamsTypeOptions } from '../ApiMessageBody/constants'
import { RequestHeaders } from '../ApiRequestEditor/components/constants'
import {ApiParamsType, BodyParamsType, ParamAttrType, commonTableSx} from "@common/const/api-detail";
import { ApiParamsType, BodyParamsType, ParamAttrType, commonTableSx } from '@common/const/api-detail'
import {
determineCheckState,
flattenTree,
generateId,
getActionColWidth,
isNil,
traverse
} from "@common/utils/postcat.tsx";
import {MoreSetting} from "../../../MoreSetting";
determineCheckState,
flattenTree,
generateId,
getActionColWidth,
isNil,
traverse
} from '@common/utils/postcat.tsx'
import { MoreSetting } from '../../../MoreSetting'
import {
AutoCompleteOption,
DataGridAutoCompleteProps,
DataGridTextFieldProps,
EditableDataGridSx
} from "../EditableDataGrid";
import {collapseTableSx} from "../../../PreviewTable";
import {IconButton} from "../../../IconButton";
import {Icon} from "../../../Icon";
import {useMoreSettingHiddenConfig} from "./hooks/useMoreSettingHiddenConfig.ts";
AutoCompleteOption,
DataGridAutoCompleteProps,
DataGridTextFieldProps,
EditableDataGridSx
} from '../EditableDataGrid'
import { collapseTableSx } from '../../../PreviewTable'
import { IconButton } from '../../../IconButton'
import { Icon } from '../../../Icon'
import { useMoreSettingHiddenConfig } from './hooks/useMoreSettingHiddenConfig.ts'
import { $t } from '@common/locales/index.ts'
export interface RenderMessageBody extends BodyParamsType {
@@ -156,7 +156,7 @@ export function MessageDataGrid(props: MessageDataGridProps<RenderMessageBody>)
useEffect(() => {
if (initialRows && loaded && !innerLoaded) {
let updateRows = [...initialRows,EmptyRow()]
let updateRows = [...initialRows, EmptyRow()]
if (!updateRows?.length && contentType !== 'XML') {
updateRows = [EmptyRow()]
}
@@ -171,7 +171,7 @@ export function MessageDataGrid(props: MessageDataGridProps<RenderMessageBody>)
onChange?.(updateRows)
setInnerLoaded(true)
}
}, [EmptyRow, contentType, initialRows, loaded,innerLoaded, onChange, updateSelectAll])
}, [EmptyRow, contentType, initialRows, loaded, innerLoaded, onChange, updateSelectAll])
useEffect(() => {
const neoRenderRows = flattenTree(
@@ -251,15 +251,13 @@ export function MessageDataGrid(props: MessageDataGridProps<RenderMessageBody>)
const getActions = useCallback(
(params: GridRowParams<RenderMessageBody>) => {
const actions = [
<IconButton title={$t("更多设置")} name="more" onClick={() => handleOpenMoreSetting(params)} />
]
const actions = [<IconButton title={$t('更多设置')} name="more" onClick={() => handleOpenMoreSetting(params)} />]
const isXML = contentType === 'XML'
const isRoot = params.row.__globalIndex__ === 0
if (['JSON', 'XML'].includes(contentType)) {
actions.unshift(
<IconButton
title={$t("添加子参数")}
title={$t('添加子参数')}
name="add"
onClick={() => {
const newRow = EmptyRow()
@@ -289,7 +287,7 @@ export function MessageDataGrid(props: MessageDataGridProps<RenderMessageBody>)
if (!(isXML && isRoot)) {
actions.unshift(
<IconButton
title={$t("向下添加行")}
title={$t('向下添加行')}
name="down-small"
onClick={() => {
const newRow = EmptyRow()
@@ -302,11 +300,11 @@ export function MessageDataGrid(props: MessageDataGridProps<RenderMessageBody>)
}
}
if (renderRows.length > 1) {
actions.push(<IconButton title={$t("删除")} name="delete" onClick={() => handleRowDelete(params)} />)
actions.push(<IconButton title={$t('删除')} name="delete" onClick={() => handleRowDelete(params)} />)
}
return actions
},
[EmptyRow, tableApiRef, contentType, handleOpenMoreSetting, handleRowDelete, renderRows.length, rows]
)
@@ -315,7 +313,7 @@ export function MessageDataGrid(props: MessageDataGridProps<RenderMessageBody>)
field: 'name',
headerName: $t('标签'),
editable: true,
sortable:false,
sortable: false,
renderEditCell: (params) => {
const options = RequestHeaders.map((option) => option.key)
return (
@@ -331,7 +329,7 @@ export function MessageDataGrid(props: MessageDataGridProps<RenderMessageBody>)
const rowIndex = params.row.__globalIndex__ as number
if (renderRows.length === rowIndex + 1) {
const newRow = EmptyRow()
setRows((preRows)=>[...preRows,newRow])
setRows((preRows) => [...preRows, newRow])
}
}}
/>
@@ -386,7 +384,7 @@ export function MessageDataGrid(props: MessageDataGridProps<RenderMessageBody>)
const newRow = EmptyRow()
const currentLevelChildrenList = params.row.parent?.childList ?? rows
currentLevelChildrenList?.splice((params.row.__levelIndex__ || 0) + 1, 0, newRow)
setRows((preRows)=>[...preRows])
setRows((preRows) => [...preRows])
}
}}
/>
@@ -444,7 +442,7 @@ export function MessageDataGrid(props: MessageDataGridProps<RenderMessageBody>)
indeterminate={selectAll === 'indeterminate'}
onChange={handleSelectAllChange}
/>
<Typography sx={{fontSize:'14px'}}>{$t('必需')}</Typography>
<Typography sx={{ fontSize: '14px' }}>{$t('必需')}</Typography>
</Box>
)
},
@@ -516,7 +514,8 @@ export function MessageDataGrid(props: MessageDataGridProps<RenderMessageBody>)
paddingRight: theme.spacing(1)
}
}}
placeholder={$t('示例')} />
placeholder={$t('示例')}
/>
)
}
},
@@ -1,9 +1,9 @@
import { InputAdornment, TextField, Select, MenuItem, Divider, SelectChangeEvent, Typography, Box } from '@mui/material'
import { InputAdornment, TextField, Select, MenuItem, Divider, SelectChangeEvent } from '@mui/material'
import { SyntheticEvent } from 'react'
import {ParseCurlResult} from "@common/const/api-detail";
import {HTTPMethod, RequestMethod} from "../../../RequestMethod";
import {ParseCurl} from "@common/utils/curl.ts";
import { $t } from '@common/locales';
import { ParseCurlResult } from '@common/const/api-detail'
import { HTTPMethod, RequestMethod } from '../../../RequestMethod'
import { ParseCurl } from '@common/utils/curl.ts'
import { $t } from '@common/locales'
interface UriInputProps {
inputValue?: string
@@ -22,8 +22,6 @@ export function UriInput({
onCURLPaste,
onTest
}: UriInputProps) {
const handleSelectChange = (event: SelectChangeEvent<HTTPMethod>) => {
onSelectChange?.(event.target.value as HTTPMethod)
}
@@ -52,14 +50,14 @@ export function UriInput({
return (
<TextField
fullWidth
placeholder={$t("输入 URL 或 cURL")}
placeholder={$t('输入 URL 或 cURL')}
value={inputValue}
onChange={handleInputChange}
sx={{
input: {
lineHeight: '40px',
fontSize: '16px',
padding:'8.5px 14px 8.5px 0'
padding: '8.5px 14px 8.5px 0'
}
}}
onKeyDown={(event) => {
@@ -1,14 +1,15 @@
import {generateId} from "@common/utils/postcat.tsx";
import { generateId } from '@common/utils/postcat.tsx'
import {
ApiBodyType,
BodyParamsType,
HeaderParamsType,
QueryParamsType, ResponseList,
QueryParamsType,
ResponseList,
RestParamsType
} from "@common/const/api-detail";
import {Protocol} from "../RequestMethod";
} from '@common/const/api-detail'
import { Protocol } from '../RequestMethod'
type SafeAny = unknown
type SafeAny = unknown
type Timestamp = number
declare interface HttpRequestMessage {
@@ -68,4 +69,4 @@ export function getDefaultApiInfo(): ApiRequest {
},
responseList: [{}]
} as unknown as ApiRequest
}
}
@@ -1,8 +1,7 @@
import { Box, Chip, Stack, Typography, Skeleton } from '@mui/material'
import {HTTPMethod, Protocol,RequestMethod} from "../../../RequestMethod";
import {Clipboard} from "../../../Clipboard"
import { $t } from '@common/locales';
import { HTTPMethod, Protocol, RequestMethod } from '../../../RequestMethod'
import { Clipboard } from '../../../Clipboard'
import { $t } from '@common/locales'
interface ApiBasicInfoDisplayProps {
apiName: string
@@ -39,15 +38,19 @@ export default function ApiBasicInfoDisplay(props: Partial<ApiBasicInfoDisplayPr
<Box display="flex">
<Stack direction="row" spacing={1} alignItems="center">
<Chip
label={$t("HTTP")}
label={$t('HTTP')}
sx={{
height:'22px',
height: '22px',
borderRadius: '4px',
color: '#fff',
backgroundColor: '#067ddb'
}}
/>
<RequestMethod variant="filled" protocol={protocol ?? Protocol.HTTP} method={method ?? 'GET' as (keyof typeof HTTPMethod)} />
<RequestMethod
variant="filled"
protocol={protocol ?? Protocol.HTTP}
method={method ?? ('GET' as keyof typeof HTTPMethod)}
/>
<Typography>
{/*{selectedEnv ? selectedEnv.hostUri : ''}*/}
{uri}

Some files were not shown because too many files have changed in this diff Show More