发布 v1.10 #5
@@ -105,9 +105,22 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
return Result.ok(pluginService.preparePluginsForCurrentUser(plugins, true, false));
|
return Result.ok(pluginService.preparePluginsForCurrentUser(plugins, true, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按分类分页查询插件,并支持按名称模糊查询。
|
||||||
|
*
|
||||||
|
* @param request 当前请求
|
||||||
|
* @param sortKey 排序字段
|
||||||
|
* @param sortType 排序方向
|
||||||
|
* @param pageNumber 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @param category 分类 ID,0 表示全部分类
|
||||||
|
* @param name 插件名称关键字
|
||||||
|
* @return 插件分页结果
|
||||||
|
*/
|
||||||
@GetMapping("/pageByCategory")
|
@GetMapping("/pageByCategory")
|
||||||
@SaCheckPermission("/api/v1/plugin/query")
|
@SaCheckPermission("/api/v1/plugin/query")
|
||||||
public Result<Page<Plugin>> pageByCategory(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize, int category) {
|
public Result<Page<Plugin>> pageByCategory(HttpServletRequest request, String sortKey, String sortType,
|
||||||
|
Long pageNumber, Long pageSize, int category, String name) {
|
||||||
if (pageNumber == null || pageNumber < 1) {
|
if (pageNumber == null || pageNumber < 1) {
|
||||||
pageNumber = 1L;
|
pageNumber = 1L;
|
||||||
}
|
}
|
||||||
@@ -120,7 +133,7 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
|
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
|
||||||
} else {
|
} else {
|
||||||
Result<Page<Plugin>> result = pluginService.pageByCategory(pageNumber, pageSize, category);
|
Result<Page<Plugin>> result = pluginService.pageByCategory(pageNumber, pageSize, category, name);
|
||||||
if (result != null && result.getData() != null) {
|
if (result != null && result.getData() != null) {
|
||||||
aiResourceCreatorNameSupport.fillPluginCreatorNames(result.getData().getRecords());
|
aiResourceCreatorNameSupport.fillPluginCreatorNames(result.getData().getRecords());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package tech.easyflow.ai.service;
|
package tech.easyflow.ai.service;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.service.IService;
|
import com.mybatisflex.core.service.IService;
|
||||||
import tech.easyflow.ai.entity.Plugin;
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
@@ -20,7 +21,16 @@ public interface PluginService extends IService<Plugin> {
|
|||||||
|
|
||||||
List<Plugin> getList();
|
List<Plugin> getList();
|
||||||
|
|
||||||
Result pageByCategory(Long pageNumber, Long pageSize, int category);
|
/**
|
||||||
|
* 按分类分页查询插件。
|
||||||
|
*
|
||||||
|
* @param pageNumber 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @param category 分类 ID
|
||||||
|
* @param name 插件名称关键字
|
||||||
|
* @return 插件分页结果
|
||||||
|
*/
|
||||||
|
Result<Page<Plugin>> pageByCategory(Long pageNumber, Long pageSize, int category, String name);
|
||||||
|
|
||||||
boolean updatePlugin(Plugin plugin);
|
boolean updatePlugin(Plugin plugin);
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Page<Plugin>> pageByCategory(Long pageNumber, Long pageSize, int category) {
|
public Result<Page<Plugin>> pageByCategory(Long pageNumber, Long pageSize, int category, String name) {
|
||||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN");
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN");
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create().select(PluginCategoryMapping::getPluginId)
|
QueryWrapper queryWrapper = QueryWrapper.create().select(PluginCategoryMapping::getPluginId)
|
||||||
.eq(PluginCategoryMapping::getCategoryId, category);
|
.eq(PluginCategoryMapping::getCategoryId, category);
|
||||||
@@ -158,7 +158,8 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
|
|||||||
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, 0L));
|
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, 0L));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Plugin> totalPlugins = preparePluginsForCurrentUser(queryPluginsByIds(visiblePluginIds), true, false);
|
List<Plugin> totalPlugins = preparePluginsForCurrentUser(
|
||||||
|
queryPluginsByIds(visiblePluginIds, name), true, false);
|
||||||
int fromIndex = Math.max(0, Math.toIntExact((pageNumber - 1) * pageSize));
|
int fromIndex = Math.max(0, Math.toIntExact((pageNumber - 1) * pageSize));
|
||||||
if (fromIndex >= totalPlugins.size()) {
|
if (fromIndex >= totalPlugins.size()) {
|
||||||
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, totalPlugins.size()));
|
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, totalPlugins.size()));
|
||||||
@@ -251,11 +252,21 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
|
|||||||
return pluginMapper.selectListByQueryAs(creatorPluginWrapper, BigInteger.class);
|
return pluginMapper.selectListByQueryAs(creatorPluginWrapper, BigInteger.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Plugin> queryPluginsByIds(List<BigInteger> pluginIds) {
|
/**
|
||||||
|
* 按给定顺序查询插件,并按名称关键字过滤。
|
||||||
|
*
|
||||||
|
* @param pluginIds 插件 ID 列表
|
||||||
|
* @param name 插件名称关键字
|
||||||
|
* @return 保持输入 ID 顺序的插件列表
|
||||||
|
*/
|
||||||
|
private List<Plugin> queryPluginsByIds(List<BigInteger> pluginIds, String name) {
|
||||||
if (CollectionUtil.isEmpty(pluginIds)) {
|
if (CollectionUtil.isEmpty(pluginIds)) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
QueryWrapper queryPluginWrapper = QueryWrapper.create().select().in(Plugin::getId, pluginIds);
|
QueryWrapper queryPluginWrapper = QueryWrapper.create().select().in(Plugin::getId, pluginIds);
|
||||||
|
if (name != null && !name.isBlank()) {
|
||||||
|
queryPluginWrapper.like(Plugin::getName, name.trim());
|
||||||
|
}
|
||||||
List<Plugin> plugins = pluginMapper.selectListWithRelationsByQuery(queryPluginWrapper);
|
List<Plugin> plugins = pluginMapper.selectListWithRelationsByQuery(queryPluginWrapper);
|
||||||
Map<BigInteger, Plugin> pluginMap = plugins.stream().collect(Collectors.toMap(
|
Map<BigInteger, Plugin> pluginMap = plugins.stream().collect(Collectors.toMap(
|
||||||
Plugin::getId,
|
Plugin::getId,
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package tech.easyflow.ai.service.impl;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
|
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
|
||||||
|
import tech.easyflow.ai.mapper.PluginMapper;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件分类分页查询测试。
|
||||||
|
*/
|
||||||
|
public class PluginServicePageQueryTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证具体分类下仍会按插件名称过滤。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldFilterCategorizedPluginsByName() {
|
||||||
|
PluginMapper pluginMapper = mock(PluginMapper.class);
|
||||||
|
PluginCategoryMappingMapper mappingMapper = mock(PluginCategoryMappingMapper.class);
|
||||||
|
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
|
||||||
|
RoleCategoryAccessSnapshot access = mock(RoleCategoryAccessSnapshot.class);
|
||||||
|
PluginServiceImpl service = new PluginServiceImpl();
|
||||||
|
service.pluginMapper = pluginMapper;
|
||||||
|
service.pluginCategoryMappingMapper = mappingMapper;
|
||||||
|
setField(service, "categoryPermissionService", categoryPermissionService);
|
||||||
|
|
||||||
|
when(categoryPermissionService.getCurrentAccess("PLUGIN")).thenReturn(access);
|
||||||
|
when(access.isRestricted()).thenReturn(false);
|
||||||
|
when(mappingMapper.selectListByQueryAs(any(QueryWrapper.class), eq(BigInteger.class)))
|
||||||
|
.thenReturn(List.of(BigInteger.ONE));
|
||||||
|
when(pluginMapper.selectListWithRelationsByQuery(any(QueryWrapper.class)))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
service.pageByCategory(1L, 12L, 7, " 1213 ");
|
||||||
|
|
||||||
|
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||||
|
verify(pluginMapper).selectListWithRelationsByQuery(queryCaptor.capture());
|
||||||
|
String sql = queryCaptor.getValue().toSQL().toLowerCase(Locale.ROOT);
|
||||||
|
Assert.assertTrue(sql.contains("name"));
|
||||||
|
Assert.assertTrue(sql.contains("like"));
|
||||||
|
Assert.assertTrue(sql.contains("1213"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过反射设置私有依赖。
|
||||||
|
*
|
||||||
|
* @param target 目标对象
|
||||||
|
* @param fieldName 字段名称
|
||||||
|
* @param value 字段值
|
||||||
|
*/
|
||||||
|
private static void setField(Object target, String fieldName, Object value) {
|
||||||
|
Class<?> current = target.getClass();
|
||||||
|
while (current != null) {
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Field field = current.getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
return;
|
||||||
|
} catch (NoSuchFieldException ignored) {
|
||||||
|
current = current.getSuperclass();
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new IllegalStateException("设置测试字段失败: " + fieldName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("未找到字段: " + fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,12 +47,19 @@ describe('detail return navigation', () => {
|
|||||||
|
|
||||||
it('returns plugin tool editing to its plugin tool list with a safe fallback', () => {
|
it('returns plugin tool editing to its plugin tool list with a safe fallback', () => {
|
||||||
const editSource = readViewSource('ai/plugin/PluginToolEdit.vue');
|
const editSource = readViewSource('ai/plugin/PluginToolEdit.vue');
|
||||||
|
const toolsSource = readViewSource('ai/plugin/PluginTools.vue');
|
||||||
|
const pluginListSource = readViewSource('ai/plugin/Plugin.vue');
|
||||||
const listSource = readViewSource('ai/plugin/PluginToolTable.vue');
|
const listSource = readViewSource('ai/plugin/PluginToolTable.vue');
|
||||||
|
|
||||||
expect(editSource).not.toMatch(/router\.(?:back|go)\s*\(/);
|
expect(editSource).not.toMatch(/router\.(?:back|go)\s*\(/);
|
||||||
expect(editSource).toContain("path: '/ai/plugin/tools'");
|
expect(editSource).toContain("path: '/ai/plugin/tools'");
|
||||||
expect(editSource).toContain("path: '/ai/plugin'");
|
expect(editSource).toContain("path: '/ai/plugin'");
|
||||||
|
expect(editSource).toContain('buildPluginToolsRouteQueryFromEdit');
|
||||||
|
expect(toolsSource).toContain('parsePluginToolsReturnState');
|
||||||
|
expect(toolsSource).toContain('buildPluginListRouteQuery');
|
||||||
|
expect(pluginListSource).toContain('buildPluginToolsReturnQuery');
|
||||||
expect(listSource).toContain('pluginId: props.pluginId');
|
expect(listSource).toContain('pluginId: props.pluginId');
|
||||||
|
expect(listSource).toContain('...route.query');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns execution steps to the filtered execution record list', () => {
|
it('returns execution steps to the filtered execution record list', () => {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { PluginListRouteState } from './plugin-route-state';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ActionButton,
|
ActionButton,
|
||||||
CardPrimaryAction,
|
CardPrimaryAction,
|
||||||
} from '#/components/page/CardList.vue';
|
} from '#/components/page/CardList.vue';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
@@ -29,7 +31,16 @@ import PageData from '#/components/page/PageData.vue';
|
|||||||
import PageSide from '#/components/page/PageSide.vue';
|
import PageSide from '#/components/page/PageSide.vue';
|
||||||
import AddPluginModal from '#/views/ai/plugin/AddPluginModal.vue';
|
import AddPluginModal from '#/views/ai/plugin/AddPluginModal.vue';
|
||||||
|
|
||||||
|
import { buildPluginPageQueryParams } from './plugin-query';
|
||||||
|
import {
|
||||||
|
buildPluginToolsReturnQuery,
|
||||||
|
mergePluginListRouteQuery,
|
||||||
|
parsePluginListRouteState,
|
||||||
|
} from './plugin-route-state';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const initialListState = parsePluginListRouteState(route.query);
|
||||||
|
|
||||||
interface PluginCategory {
|
interface PluginCategory {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -61,6 +72,7 @@ function openPluginTools(item: PluginRecord) {
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
pageKey: '/ai/plugin',
|
pageKey: '/ai/plugin',
|
||||||
navTitle: resolveNavTitle(item),
|
navTitle: resolveNavTitle(item),
|
||||||
|
...buildPluginToolsReturnQuery(currentListState()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -100,6 +112,15 @@ const pluginTypeTagMap = {
|
|||||||
2: $t('plugin.typeWorkflow'),
|
2: $t('plugin.typeWorkflow'),
|
||||||
};
|
};
|
||||||
const categoryList = ref<PluginCategory[]>([]);
|
const categoryList = ref<PluginCategory[]>([]);
|
||||||
|
const selectedCategoryId = ref(initialListState.categoryId);
|
||||||
|
const searchKeyword = ref(initialListState.keyword.trim());
|
||||||
|
const currentPageNumber = ref(initialListState.pageNumber);
|
||||||
|
const currentPageSize = ref(initialListState.pageSize);
|
||||||
|
const suppressInitialCategoryChange = ref(true);
|
||||||
|
const initialQueryParams = buildPluginPageQueryParams(
|
||||||
|
initialListState.categoryId,
|
||||||
|
initialListState.keyword,
|
||||||
|
);
|
||||||
const controlBtns = [
|
const controlBtns = [
|
||||||
{
|
{
|
||||||
icon: Edit,
|
icon: Edit,
|
||||||
@@ -138,6 +159,17 @@ const getPluginCategoryList = async () => {
|
|||||||
{ id: '0', name: $t('common.allCategories') },
|
{ id: '0', name: $t('common.allCategories') },
|
||||||
...serverCategories,
|
...serverCategories,
|
||||||
];
|
];
|
||||||
|
if (
|
||||||
|
selectedCategoryId.value !== '0' &&
|
||||||
|
!serverCategories.some(
|
||||||
|
(category) =>
|
||||||
|
String(category.id) === String(selectedCategoryId.value),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
suppressInitialCategoryChange.value = false;
|
||||||
|
selectedCategoryId.value = '0';
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -154,7 +186,7 @@ const handleDelete = (item: PluginRecord) => {
|
|||||||
api.post('/api/v1/plugin/plugin/remove', { id: item.id }).then((res) => {
|
api.post('/api/v1/plugin/plugin/remove', { id: item.id }).then((res) => {
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success($t('message.deleteOkMessage'));
|
ElMessage.success($t('message.deleteOkMessage'));
|
||||||
pageDataRef.value.setQuery({});
|
pageDataRef.value?.reload?.();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -172,7 +204,6 @@ const headerButtons = [
|
|||||||
data: { action: 'add' },
|
data: { action: 'add' },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const pluginCategoryId = ref('0');
|
|
||||||
const dialogVisible = ref(false); // 弹窗显隐
|
const dialogVisible = ref(false); // 弹窗显隐
|
||||||
const isEdit = ref(false); // 是否为编辑模式
|
const isEdit = ref(false); // 是否为编辑模式
|
||||||
const formData = ref<CategoryFormData>({ name: '', id: '' });
|
const formData = ref<CategoryFormData>({ name: '', id: '' });
|
||||||
@@ -195,8 +226,48 @@ const handleButtonClick = (event: HeaderActionEvent, _item: unknown) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
pageDataRef.value?.setQuery(
|
||||||
|
buildPluginPageQueryParams(selectedCategoryId.value, searchKeyword.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentListState(): PluginListRouteState {
|
||||||
|
const pageState = pageDataRef.value?.getPageState?.();
|
||||||
|
return {
|
||||||
|
categoryId: String(selectedCategoryId.value || '0'),
|
||||||
|
keyword: searchKeyword.value,
|
||||||
|
pageNumber: pageState?.pageNumber ?? currentPageNumber.value,
|
||||||
|
pageSize: pageState?.pageSize ?? currentPageSize.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncListRouteState() {
|
||||||
|
const target = {
|
||||||
|
path: '/ai/plugin',
|
||||||
|
query: mergePluginListRouteQuery(route.query, currentListState()),
|
||||||
|
};
|
||||||
|
if (router.resolve(target).fullPath !== route.fullPath) {
|
||||||
|
void router.replace(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageStateChange(state: {
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
}) {
|
||||||
|
currentPageNumber.value = state.pageNumber;
|
||||||
|
currentPageSize.value = state.pageSize;
|
||||||
|
syncListRouteState();
|
||||||
|
}
|
||||||
|
|
||||||
const handleSearch = (params?: string) => {
|
const handleSearch = (params?: string) => {
|
||||||
pageDataRef.value.setQuery({ title: params ?? '', isQueryOr: true });
|
searchKeyword.value = params?.trim() ?? '';
|
||||||
|
applyFilters();
|
||||||
|
};
|
||||||
|
const reloadCurrentList = () => {
|
||||||
|
pageDataRef.value?.reload?.();
|
||||||
};
|
};
|
||||||
const handleEditCategory = (params: CategoryFormData) => {
|
const handleEditCategory = (params: CategoryFormData) => {
|
||||||
api
|
api
|
||||||
@@ -230,7 +301,17 @@ const handleDeleteCategory = (params: PluginCategory) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const handleClickCategory = (item: PluginCategory) => {
|
const handleClickCategory = (item: PluginCategory) => {
|
||||||
pageDataRef.value.setQuery({ category: item.id });
|
if (
|
||||||
|
suppressInitialCategoryChange.value &&
|
||||||
|
String(item.id) === initialListState.categoryId
|
||||||
|
) {
|
||||||
|
suppressInitialCategoryChange.value = false;
|
||||||
|
selectedCategoryId.value = item.id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
suppressInitialCategoryChange.value = false;
|
||||||
|
selectedCategoryId.value = item.id;
|
||||||
|
applyFilters();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -239,6 +320,7 @@ const handleClickCategory = (item: PluginCategory) => {
|
|||||||
<div class="knowledge-header">
|
<div class="knowledge-header">
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
|
:initial-value="searchKeyword"
|
||||||
:search-placeholder="$t('plugin.searchUsers')"
|
:search-placeholder="$t('plugin.searchUsers')"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="handleButtonClick"
|
@button-click="handleButtonClick"
|
||||||
@@ -252,7 +334,7 @@ const handleClickCategory = (item: PluginCategory) => {
|
|||||||
:menus="categoryList"
|
:menus="categoryList"
|
||||||
:control-btns="controlBtns"
|
:control-btns="controlBtns"
|
||||||
:footer-button="footerButton"
|
:footer-button="footerButton"
|
||||||
default-selected="0"
|
:default-selected="selectedCategoryId"
|
||||||
@change="handleClickCategory"
|
@change="handleClickCategory"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,9 +343,11 @@ const handleClickCategory = (item: PluginCategory) => {
|
|||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/plugin/pageByCategory"
|
page-url="/api/v1/plugin/pageByCategory"
|
||||||
:page-size="12"
|
:page-size="initialListState.pageSize"
|
||||||
:page-sizes="[12, 24, 36, 48]"
|
:page-sizes="[12, 24, 36, 48]"
|
||||||
:extra-query-params="{ category: pluginCategoryId }"
|
:initial-page-number="initialListState.pageNumber"
|
||||||
|
:initial-query-params="initialQueryParams"
|
||||||
|
@state-change="handlePageStateChange"
|
||||||
>
|
>
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<CardPage
|
<CardPage
|
||||||
@@ -294,7 +378,7 @@ const handleClickCategory = (item: PluginCategory) => {
|
|||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AddPluginModal ref="aiPluginModalRef" @reload="handleSearch" />
|
<AddPluginModal ref="aiPluginModalRef" @reload="reloadCurrentList" />
|
||||||
<EasyFlowFormModal
|
<EasyFlowFormModal
|
||||||
:title="isEdit ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
:title="isEdit ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||||||
v-model:open="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ import PluginInputAndOutParams from '#/views/ai/plugin/PluginInputAndOutParams.v
|
|||||||
import PluginRunTestModal from '#/views/ai/plugin/PluginRunTestModal.vue';
|
import PluginRunTestModal from '#/views/ai/plugin/PluginRunTestModal.vue';
|
||||||
import WorkflowApprovalSnapshotPreview from '#/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue';
|
import WorkflowApprovalSnapshotPreview from '#/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildPluginListRouteQuery,
|
||||||
|
buildPluginToolsRouteQueryFromEdit,
|
||||||
|
parsePluginToolsReturnState,
|
||||||
|
} from './plugin-route-state';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -168,11 +174,14 @@ function back() {
|
|||||||
if (pluginId) {
|
if (pluginId) {
|
||||||
void router.replace({
|
void router.replace({
|
||||||
path: '/ai/plugin/tools',
|
path: '/ai/plugin/tools',
|
||||||
query: { id: pluginId },
|
query: buildPluginToolsRouteQueryFromEdit(route.query, pluginId),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void router.replace({ path: '/ai/plugin' });
|
void router.replace({
|
||||||
|
path: '/ai/plugin',
|
||||||
|
query: buildPluginListRouteQuery(parsePluginToolsReturnState(route.query)),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePluginTool(index: number) {
|
function updatePluginTool(index: number) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ import { api } from '#/api/request';
|
|||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import AiPluginToolModal from '#/views/ai/plugin/AiPluginToolModal.vue';
|
import AiPluginToolModal from '#/views/ai/plugin/AiPluginToolModal.vue';
|
||||||
|
|
||||||
|
import { buildPluginToolPageQueryParams } from './plugin-query';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
pluginId: {
|
pluginId: {
|
||||||
required: true,
|
required: true,
|
||||||
@@ -29,21 +31,39 @@ const props = defineProps({
|
|||||||
default: 1,
|
default: 1,
|
||||||
type: Number,
|
type: Number,
|
||||||
},
|
},
|
||||||
|
initialKeyword: {
|
||||||
|
default: '',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
initialPageNumber: {
|
||||||
|
default: 1,
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
initialPageSize: {
|
||||||
|
default: 10,
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
e: 'stateChange',
|
||||||
|
state: {
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
},
|
||||||
|
): void;
|
||||||
|
}>();
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openPluginToolModal() {
|
openPluginToolModal() {
|
||||||
aiPluginToolRef.value.openDialog();
|
aiPluginToolRef.value.openDialog();
|
||||||
},
|
},
|
||||||
reload: () => {
|
reload: () => {
|
||||||
pageDataRef.value.setQuery({ pluginId: props.pluginId });
|
pageDataRef.value?.reload?.();
|
||||||
},
|
},
|
||||||
handleSearch: (params: string) => {
|
handleSearch: (params: string) => {
|
||||||
pageDataRef.value.setQuery({
|
pageDataRef.value.setQuery(buildPluginToolPageQueryParams(params));
|
||||||
pluginId: props.pluginId,
|
|
||||||
isQueryOr: true,
|
|
||||||
name: params,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const pageDataRef = ref();
|
const pageDataRef = ref();
|
||||||
@@ -51,6 +71,7 @@ const handleEdit = (row: any) => {
|
|||||||
router.push({
|
router.push({
|
||||||
path: '/ai/plugin/tool/edit',
|
path: '/ai/plugin/tool/edit',
|
||||||
query: {
|
query: {
|
||||||
|
...route.query,
|
||||||
id: row.id,
|
id: row.id,
|
||||||
pageKey: '/ai/plugin',
|
pageKey: '/ai/plugin',
|
||||||
pluginId: props.pluginId,
|
pluginId: props.pluginId,
|
||||||
@@ -67,14 +88,20 @@ const handleDelete = (row: any) => {
|
|||||||
api.post('/api/v1/pluginItem/remove', { id: row.id }).then((res) => {
|
api.post('/api/v1/pluginItem/remove', { id: row.id }).then((res) => {
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success($t('message.deleteOkMessage'));
|
ElMessage.success($t('message.deleteOkMessage'));
|
||||||
pageDataRef.value.setQuery({ pluginId: props.pluginId });
|
pageDataRef.value?.reload?.();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const aiPluginToolRef = ref();
|
const aiPluginToolRef = ref();
|
||||||
const pluginToolReload = () => {
|
const pluginToolReload = () => {
|
||||||
pageDataRef.value.setQuery({ pluginId: props.pluginId });
|
pageDataRef.value?.reload?.();
|
||||||
|
};
|
||||||
|
const handlePageStateChange = (state: {
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
}) => {
|
||||||
|
emit('stateChange', state);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -82,8 +109,11 @@ const pluginToolReload = () => {
|
|||||||
<PageData
|
<PageData
|
||||||
page-url="/api/v1/pluginItem/page"
|
page-url="/api/v1/pluginItem/page"
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
:page-size="10"
|
:page-size="props.initialPageSize"
|
||||||
|
:initial-page-number="props.initialPageNumber"
|
||||||
|
:initial-query-params="buildPluginToolPageQueryParams(props.initialKeyword)"
|
||||||
:extra-query-params="{ pluginId: props.pluginId }"
|
:extra-query-params="{ pluginId: props.pluginId }"
|
||||||
|
@state-change="handlePageStateChange"
|
||||||
>
|
>
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<ElTable :data="pageList" style="width: 100%" size="large">
|
<ElTable :data="pageList" style="width: 100%" size="large">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { PluginToolListRouteState } from './plugin-route-state';
|
||||||
|
|
||||||
import { computed, markRaw, onMounted, ref } from 'vue';
|
import { computed, markRaw, onMounted, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
@@ -10,12 +12,23 @@ import { api } from '#/api/request';
|
|||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
import PluginToolTable from '#/views/ai/plugin/PluginToolTable.vue';
|
import PluginToolTable from '#/views/ai/plugin/PluginToolTable.vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildPluginListRouteQuery,
|
||||||
|
mergePluginToolListRouteQuery,
|
||||||
|
parsePluginToolListRouteState,
|
||||||
|
parsePluginToolsReturnState,
|
||||||
|
} from './plugin-route-state';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const initialToolListState = parsePluginToolListRouteState(route.query);
|
||||||
|
|
||||||
const pluginId = ref<string>((route.query.id as string) || '');
|
const pluginId = ref<string>((route.query.id as string) || '');
|
||||||
const pluginInfo = ref<any>({});
|
const pluginInfo = ref<any>({});
|
||||||
const pluginToolRef = ref();
|
const pluginToolRef = ref();
|
||||||
|
const toolSearchKeyword = ref(initialToolListState.keyword.trim());
|
||||||
|
const currentToolPageNumber = ref(initialToolListState.pageNumber);
|
||||||
|
const currentToolPageSize = ref(initialToolListState.pageSize);
|
||||||
|
|
||||||
const headerButtons = computed<any[]>(() => {
|
const headerButtons = computed<any[]>(() => {
|
||||||
const buttons: any[] = [
|
const buttons: any[] = [
|
||||||
@@ -52,14 +65,47 @@ async function loadPluginInfo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch(params: any) {
|
function currentToolListState(): PluginToolListRouteState {
|
||||||
pluginToolRef.value.handleSearch(params);
|
return {
|
||||||
|
keyword: toolSearchKeyword.value,
|
||||||
|
pageNumber: currentToolPageNumber.value,
|
||||||
|
pageSize: currentToolPageSize.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncToolListRouteState() {
|
||||||
|
const target = {
|
||||||
|
path: '/ai/plugin/tools',
|
||||||
|
query: mergePluginToolListRouteQuery(route.query, currentToolListState()),
|
||||||
|
};
|
||||||
|
if (router.resolve(target).fullPath !== route.fullPath) {
|
||||||
|
void router.replace(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToolPageStateChange(state: {
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
}) {
|
||||||
|
currentToolPageNumber.value = state.pageNumber;
|
||||||
|
currentToolPageSize.value = state.pageSize;
|
||||||
|
syncToolListRouteState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch(params: string) {
|
||||||
|
toolSearchKeyword.value = params.trim();
|
||||||
|
pluginToolRef.value.handleSearch(toolSearchKeyword.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleButtonClick(event: any) {
|
function handleButtonClick(event: any) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'back': {
|
case 'back': {
|
||||||
router.push({ path: '/ai/plugin' });
|
void router.replace({
|
||||||
|
path: '/ai/plugin',
|
||||||
|
query: buildPluginListRouteQuery(
|
||||||
|
parsePluginToolsReturnState(route.query),
|
||||||
|
),
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'createTool': {
|
case 'createTool': {
|
||||||
@@ -74,6 +120,7 @@ function handleButtonClick(event: any) {
|
|||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
|
:initial-value="toolSearchKeyword"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="handleButtonClick"
|
@button-click="handleButtonClick"
|
||||||
/>
|
/>
|
||||||
@@ -83,6 +130,10 @@ function handleButtonClick(event: any) {
|
|||||||
ref="pluginToolRef"
|
ref="pluginToolRef"
|
||||||
:plugin-id="pluginId"
|
:plugin-id="pluginId"
|
||||||
:plugin-type="pluginInfo.type"
|
:plugin-type="pluginInfo.type"
|
||||||
|
:initial-keyword="initialToolListState.keyword"
|
||||||
|
:initial-page-number="initialToolListState.pageNumber"
|
||||||
|
:initial-page-size="initialToolListState.pageSize"
|
||||||
|
@state-change="handleToolPageStateChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildPluginPageQueryParams,
|
||||||
|
buildPluginToolPageQueryParams,
|
||||||
|
} from './plugin-query';
|
||||||
|
|
||||||
|
describe('plugin query params', () => {
|
||||||
|
it('uses the persisted name field and an explicit like operator', () => {
|
||||||
|
expect(buildPluginPageQueryParams('7', ' 1213 ')).toEqual({
|
||||||
|
category: '7',
|
||||||
|
name: '1213',
|
||||||
|
name__op: 'like',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retains category while clearing the keyword', () => {
|
||||||
|
expect(buildPluginPageQueryParams('7', ' ')).toEqual({
|
||||||
|
category: '7',
|
||||||
|
name: undefined,
|
||||||
|
name__op: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses contains matching for plugin tool names', () => {
|
||||||
|
expect(buildPluginToolPageQueryParams(' HTTP ')).toEqual({
|
||||||
|
name: 'HTTP',
|
||||||
|
name__op: 'like',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
22
easyflow-ui-admin/app/src/views/ai/plugin/plugin-query.ts
Normal file
22
easyflow-ui-admin/app/src/views/ai/plugin/plugin-query.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
function normalizeKeyword(keyword?: string): string {
|
||||||
|
return keyword?.trim() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPluginPageQueryParams(categoryId: string, keyword?: string) {
|
||||||
|
const name = normalizeKeyword(keyword);
|
||||||
|
return {
|
||||||
|
category: categoryId || '0',
|
||||||
|
name: name || undefined,
|
||||||
|
name__op: name ? 'like' : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPluginToolPageQueryParams(keyword?: string) {
|
||||||
|
const name = normalizeKeyword(keyword);
|
||||||
|
return {
|
||||||
|
name: name || undefined,
|
||||||
|
name__op: name ? 'like' : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { buildPluginPageQueryParams, buildPluginToolPageQueryParams };
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildPluginListRouteQuery,
|
||||||
|
buildPluginToolsReturnQuery,
|
||||||
|
buildPluginToolsRouteQueryFromEdit,
|
||||||
|
mergePluginListRouteQuery,
|
||||||
|
mergePluginToolListRouteQuery,
|
||||||
|
parsePluginListRouteState,
|
||||||
|
parsePluginToolListRouteState,
|
||||||
|
parsePluginToolsReturnState,
|
||||||
|
} from './plugin-route-state';
|
||||||
|
|
||||||
|
describe('plugin route state', () => {
|
||||||
|
it('restores plugin list page, category and keyword', () => {
|
||||||
|
expect(
|
||||||
|
parsePluginListRouteState({
|
||||||
|
categoryId: '7',
|
||||||
|
keyword: '天气',
|
||||||
|
pageNumber: '3',
|
||||||
|
pageSize: '24',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
categoryId: '7',
|
||||||
|
keyword: '天气',
|
||||||
|
pageNumber: 3,
|
||||||
|
pageSize: 24,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('round trips the plugin list state through plugin tools', () => {
|
||||||
|
const state = {
|
||||||
|
categoryId: '7',
|
||||||
|
keyword: '天气',
|
||||||
|
pageNumber: 3,
|
||||||
|
pageSize: 24,
|
||||||
|
};
|
||||||
|
const returnQuery = buildPluginToolsReturnQuery(state);
|
||||||
|
|
||||||
|
expect(returnQuery).toEqual({
|
||||||
|
returnCategoryId: '7',
|
||||||
|
returnKeyword: '天气',
|
||||||
|
returnPageNumber: '3',
|
||||||
|
returnPageSize: '24',
|
||||||
|
});
|
||||||
|
expect(parsePluginToolsReturnState(returnQuery)).toEqual(state);
|
||||||
|
expect(buildPluginListRouteQuery(state)).toEqual({
|
||||||
|
categoryId: '7',
|
||||||
|
keyword: '天气',
|
||||||
|
pageNumber: '3',
|
||||||
|
pageSize: '24',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back safely for invalid plugin and tool pagination', () => {
|
||||||
|
expect(
|
||||||
|
parsePluginListRouteState({
|
||||||
|
pageNumber: '-1',
|
||||||
|
pageSize: '999',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
categoryId: '0',
|
||||||
|
keyword: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 12,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
parsePluginToolListRouteState({
|
||||||
|
toolPageNumber: '0',
|
||||||
|
toolPageSize: '999',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
keyword: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves unrelated route query while updating list state', () => {
|
||||||
|
expect(
|
||||||
|
mergePluginListRouteQuery(
|
||||||
|
{
|
||||||
|
devLogin: 'admin',
|
||||||
|
keyword: '旧关键字',
|
||||||
|
pageNumber: '4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categoryId: '0',
|
||||||
|
keyword: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 12,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual({ devLogin: 'admin' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps plugin return context when tool list state changes', () => {
|
||||||
|
expect(
|
||||||
|
mergePluginToolListRouteQuery(
|
||||||
|
{
|
||||||
|
id: '88',
|
||||||
|
returnCategoryId: '7',
|
||||||
|
returnPageNumber: '3',
|
||||||
|
toolKeyword: '旧工具',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keyword: '查询工具',
|
||||||
|
pageNumber: 2,
|
||||||
|
pageSize: 20,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
id: '88',
|
||||||
|
returnCategoryId: '7',
|
||||||
|
returnPageNumber: '3',
|
||||||
|
toolKeyword: '查询工具',
|
||||||
|
toolPageNumber: '2',
|
||||||
|
toolPageSize: '20',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores plugin tools query after editing a tool', () => {
|
||||||
|
expect(
|
||||||
|
buildPluginToolsRouteQueryFromEdit(
|
||||||
|
{
|
||||||
|
id: 'tool-1',
|
||||||
|
pluginId: 'plugin-1',
|
||||||
|
returnCategoryId: '7',
|
||||||
|
toolPageNumber: '2',
|
||||||
|
},
|
||||||
|
'plugin-1',
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
id: 'plugin-1',
|
||||||
|
returnCategoryId: '7',
|
||||||
|
toolPageNumber: '2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
197
easyflow-ui-admin/app/src/views/ai/plugin/plugin-route-state.ts
Normal file
197
easyflow-ui-admin/app/src/views/ai/plugin/plugin-route-state.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import type { LocationQuery } from 'vue-router';
|
||||||
|
|
||||||
|
const DEFAULT_PLUGIN_PAGE_NUMBER = 1;
|
||||||
|
const DEFAULT_PLUGIN_PAGE_SIZE = 12;
|
||||||
|
const DEFAULT_PLUGIN_CATEGORY_ID = '0';
|
||||||
|
const PLUGIN_PAGE_SIZES = new Set([12, 24, 36, 48]);
|
||||||
|
|
||||||
|
const DEFAULT_TOOL_PAGE_NUMBER = 1;
|
||||||
|
const DEFAULT_TOOL_PAGE_SIZE = 10;
|
||||||
|
const TOOL_PAGE_SIZES = new Set([10, 20, 50, 100]);
|
||||||
|
|
||||||
|
const PLUGIN_LIST_QUERY_KEYS = {
|
||||||
|
categoryId: 'categoryId',
|
||||||
|
keyword: 'keyword',
|
||||||
|
pageNumber: 'pageNumber',
|
||||||
|
pageSize: 'pageSize',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const PLUGIN_RETURN_QUERY_KEYS = {
|
||||||
|
categoryId: 'returnCategoryId',
|
||||||
|
keyword: 'returnKeyword',
|
||||||
|
pageNumber: 'returnPageNumber',
|
||||||
|
pageSize: 'returnPageSize',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const TOOL_LIST_QUERY_KEYS = {
|
||||||
|
keyword: 'toolKeyword',
|
||||||
|
pageNumber: 'toolPageNumber',
|
||||||
|
pageSize: 'toolPageSize',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
interface PluginListRouteState {
|
||||||
|
categoryId: string;
|
||||||
|
keyword: string;
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginToolListRouteState {
|
||||||
|
keyword: string;
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readQueryValue(query: LocationQuery, key: string): string {
|
||||||
|
const value = query[key];
|
||||||
|
const normalized = Array.isArray(value) ? value[0] : value;
|
||||||
|
return normalized === null || normalized === undefined
|
||||||
|
? ''
|
||||||
|
: String(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePositiveInteger(value: string, fallback: number): number {
|
||||||
|
if (!/^\d+$/.test(value)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePluginState(
|
||||||
|
query: LocationQuery,
|
||||||
|
keys: typeof PLUGIN_LIST_QUERY_KEYS | typeof PLUGIN_RETURN_QUERY_KEYS,
|
||||||
|
): PluginListRouteState {
|
||||||
|
const pageSize = parsePositiveInteger(
|
||||||
|
readQueryValue(query, keys.pageSize),
|
||||||
|
DEFAULT_PLUGIN_PAGE_SIZE,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
categoryId:
|
||||||
|
readQueryValue(query, keys.categoryId) || DEFAULT_PLUGIN_CATEGORY_ID,
|
||||||
|
keyword: readQueryValue(query, keys.keyword),
|
||||||
|
pageNumber: parsePositiveInteger(
|
||||||
|
readQueryValue(query, keys.pageNumber),
|
||||||
|
DEFAULT_PLUGIN_PAGE_NUMBER,
|
||||||
|
),
|
||||||
|
pageSize: PLUGIN_PAGE_SIZES.has(pageSize)
|
||||||
|
? pageSize
|
||||||
|
: DEFAULT_PLUGIN_PAGE_SIZE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPluginStateQuery(
|
||||||
|
state: PluginListRouteState,
|
||||||
|
keys: typeof PLUGIN_LIST_QUERY_KEYS | typeof PLUGIN_RETURN_QUERY_KEYS,
|
||||||
|
): LocationQuery {
|
||||||
|
return {
|
||||||
|
...(state.pageNumber === DEFAULT_PLUGIN_PAGE_NUMBER
|
||||||
|
? {}
|
||||||
|
: { [keys.pageNumber]: String(state.pageNumber) }),
|
||||||
|
...(state.pageSize === DEFAULT_PLUGIN_PAGE_SIZE
|
||||||
|
? {}
|
||||||
|
: { [keys.pageSize]: String(state.pageSize) }),
|
||||||
|
...(state.categoryId === DEFAULT_PLUGIN_CATEGORY_ID
|
||||||
|
? {}
|
||||||
|
: { [keys.categoryId]: state.categoryId }),
|
||||||
|
...(state.keyword ? { [keys.keyword]: state.keyword } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePluginListRouteState(query: LocationQuery): PluginListRouteState {
|
||||||
|
return parsePluginState(query, PLUGIN_LIST_QUERY_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePluginToolsReturnState(
|
||||||
|
query: LocationQuery,
|
||||||
|
): PluginListRouteState {
|
||||||
|
return parsePluginState(query, PLUGIN_RETURN_QUERY_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPluginListRouteQuery(state: PluginListRouteState): LocationQuery {
|
||||||
|
return buildPluginStateQuery(state, PLUGIN_LIST_QUERY_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPluginToolsReturnQuery(
|
||||||
|
state: PluginListRouteState,
|
||||||
|
): LocationQuery {
|
||||||
|
return buildPluginStateQuery(state, PLUGIN_RETURN_QUERY_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergePluginListRouteQuery(
|
||||||
|
query: LocationQuery,
|
||||||
|
state: PluginListRouteState,
|
||||||
|
): LocationQuery {
|
||||||
|
const listKeys = new Set<string>(Object.values(PLUGIN_LIST_QUERY_KEYS));
|
||||||
|
return {
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(query).filter(([key]) => !listKeys.has(key)),
|
||||||
|
),
|
||||||
|
...buildPluginListRouteQuery(state),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePluginToolListRouteState(
|
||||||
|
query: LocationQuery,
|
||||||
|
): PluginToolListRouteState {
|
||||||
|
const pageSize = parsePositiveInteger(
|
||||||
|
readQueryValue(query, TOOL_LIST_QUERY_KEYS.pageSize),
|
||||||
|
DEFAULT_TOOL_PAGE_SIZE,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
keyword: readQueryValue(query, TOOL_LIST_QUERY_KEYS.keyword),
|
||||||
|
pageNumber: parsePositiveInteger(
|
||||||
|
readQueryValue(query, TOOL_LIST_QUERY_KEYS.pageNumber),
|
||||||
|
DEFAULT_TOOL_PAGE_NUMBER,
|
||||||
|
),
|
||||||
|
pageSize: TOOL_PAGE_SIZES.has(pageSize) ? pageSize : DEFAULT_TOOL_PAGE_SIZE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPluginToolListRouteQuery(
|
||||||
|
state: PluginToolListRouteState,
|
||||||
|
): LocationQuery {
|
||||||
|
return {
|
||||||
|
...(state.pageNumber === DEFAULT_TOOL_PAGE_NUMBER
|
||||||
|
? {}
|
||||||
|
: { [TOOL_LIST_QUERY_KEYS.pageNumber]: String(state.pageNumber) }),
|
||||||
|
...(state.pageSize === DEFAULT_TOOL_PAGE_SIZE
|
||||||
|
? {}
|
||||||
|
: { [TOOL_LIST_QUERY_KEYS.pageSize]: String(state.pageSize) }),
|
||||||
|
...(state.keyword ? { [TOOL_LIST_QUERY_KEYS.keyword]: state.keyword } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergePluginToolListRouteQuery(
|
||||||
|
query: LocationQuery,
|
||||||
|
state: PluginToolListRouteState,
|
||||||
|
): LocationQuery {
|
||||||
|
const listKeys = new Set<string>(Object.values(TOOL_LIST_QUERY_KEYS));
|
||||||
|
return {
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(query).filter(([key]) => !listKeys.has(key)),
|
||||||
|
),
|
||||||
|
...buildPluginToolListRouteQuery(state),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPluginToolsRouteQueryFromEdit(
|
||||||
|
query: LocationQuery,
|
||||||
|
pluginId: string,
|
||||||
|
): LocationQuery {
|
||||||
|
const nextQuery: LocationQuery = { ...query, id: pluginId };
|
||||||
|
delete nextQuery.pluginId;
|
||||||
|
return nextQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildPluginListRouteQuery,
|
||||||
|
buildPluginToolsReturnQuery,
|
||||||
|
buildPluginToolsRouteQueryFromEdit,
|
||||||
|
mergePluginListRouteQuery,
|
||||||
|
mergePluginToolListRouteQuery,
|
||||||
|
parsePluginListRouteState,
|
||||||
|
parsePluginToolListRouteState,
|
||||||
|
parsePluginToolsReturnState,
|
||||||
|
};
|
||||||
|
export type { PluginListRouteState, PluginToolListRouteState };
|
||||||
Reference in New Issue
Block a user