fix(全局): bug修复&XPath支持 html格式

This commit is contained in:
baiqi 2024-04-18 18:14:07 +08:00 committed by 刘瑞斌
parent 0b3f7b40b2
commit 6103b1abd2
29 changed files with 242 additions and 71 deletions

View File

@ -70,6 +70,7 @@
"pinia": "^2.1.6", "pinia": "^2.1.6",
"pinia-plugin-persistedstate": "^3.2.0", "pinia-plugin-persistedstate": "^3.2.0",
"pm": "link:@/tiptap/pm", "pm": "link:@/tiptap/pm",
"pretty": "^2.0.0",
"query-string": "^8.1.0", "query-string": "^8.1.0",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",

View File

@ -149,7 +149,7 @@ export function batchOptionScenario(
data: { data: {
moduleIds: string[]; moduleIds: string[];
selectAll: boolean; selectAll: boolean;
condition: { keyword: string }; condition: { keyword: string; filter: Record<string, any> };
excludeIds: any[]; excludeIds: any[];
selectIds: any[]; selectIds: any[];
projectId: string; projectId: string;

View File

@ -594,6 +594,9 @@
projectId: innerProject.value, projectId: innerProject.value,
sourceId: props.caseId, sourceId: props.caseId,
totalCount: propsRes.value.msPagination?.total, totalCount: propsRes.value.msPagination?.total,
condition: {
keyword: keyword.value,
},
}; };
emit('save', params); emit('save', params);

View File

@ -164,6 +164,7 @@
}, },
{ {
title: '', title: '',
dataIndex: 'operation',
slotName: 'operation', slotName: 'operation',
width: 50, width: 50,
}, },

View File

@ -7,7 +7,7 @@ export default {
'ms.personal.apiKey': 'APIKEY', 'ms.personal.apiKey': 'APIKEY',
'ms.personal.tripartite': '三方平台账号', 'ms.personal.tripartite': '三方平台账号',
'ms.personal.changeAvatar': '更换头像', 'ms.personal.changeAvatar': '更换头像',
'ms.personal.name': '用户名称', 'ms.personal.name': '姓名',
'ms.personal.namePlaceholder': '请输入用户名称', 'ms.personal.namePlaceholder': '请输入用户名称',
'ms.personal.nameRequired': '用户名称不能为空', 'ms.personal.nameRequired': '用户名称不能为空',
'ms.personal.email': '邮箱', 'ms.personal.email': '邮箱',

View File

@ -13,12 +13,13 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { XpathNode } from './types'; import { XpathNode } from './types';
import HtmlBeautify from 'pretty';
import XmlBeautify from 'xml-beautify'; import XmlBeautify from 'xml-beautify';
const props = defineProps<{ const props = defineProps<{
xmlString: string; xmlString: string;
}>(); }>();
const emit = defineEmits(['pick']); const emit = defineEmits(['pick', 'init']);
const { t } = useI18n(); const { t } = useI18n();
@ -74,12 +75,98 @@
} }
} }
/**
* 将html扁平化
* @param node html节点
* @param currentPath 当前路径
*/
function flattenHtml(node: HTMLElement | Element, currentPath: string) {
const sameNameSiblings = getSameNameSiblings(node);
if (sameNameSiblings.length > 1) {
const sameNodesIndex = document.evaluate(
`count(ancestor-or-self::${node.nodeName.toLowerCase()}/preceding-sibling::${node.nodeName.toLowerCase()}) + 1`,
node,
null,
XPathResult.NUMBER_TYPE,
null
).numberValue;
const xpath = `${currentPath}/${node.nodeName.toLowerCase()}[${sameNodesIndex}]`;
tempXmls.value.push({ content: node.nodeName.toLowerCase(), xpath });
const children = Array.from(node.children);
children.forEach((child) => {
flattenHtml(child, xpath);
});
} else {
const xpath = `${currentPath}/${node.nodeName.toLowerCase()}`;
tempXmls.value.push({ content: node.nodeName.toLowerCase(), xpath });
const children = Array.from(node.children);
children.forEach((child) => {
flattenHtml(child, xpath);
});
}
}
function copyXPath(xpath: string) { function copyXPath(xpath: string) {
if (xpath) { if (xpath) {
emit('pick', xpath); emit('pick', xpath);
} }
} }
/**
* 替换文档
* @param beautifyDoc 格式化后的文档
*/
function replaceDoc(beautifyDoc: string) {
// HTML icon
flattenedXml.value = beautifyDoc
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/(&lt;([^/][^&]*?)&gt;)/g, '<span style="color: rgb(var(--primary-5));cursor: pointer">$1📋</span>')
.split(/\r?\n/)
.map((e) => ({ content: e, xpath: '' }));
}
/**
* 解析html
*/
function parseHtml() {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(props.xmlString, 'text/html');
// parsererror HTML
const htmlErrors = xmlDoc.getElementsByTagName('parsererror');
if (htmlErrors.length > 0) {
isValidXml.value = false;
return;
}
isValidXml.value = true;
parsedXml.value = xmlDoc;
const beautifyDoc = HtmlBeautify(props.xmlString, { ocd: true });
replaceDoc(beautifyDoc);
// HTML xpath
flattenHtml(xmlDoc.documentElement, '');
// XML/HTML xpath xpath
flattenedXml.value = flattenedXml.value
.map((e) => {
const targetNodeIndex = tempXmls.value.findIndex((txt) => e.content.includes(`&lt;${txt.content}`));
if (targetNodeIndex >= 0) {
const { xpath } = tempXmls.value[targetNodeIndex];
tempXmls.value.splice(targetNodeIndex, 1); // tempXmls
return {
...e,
xpath,
};
}
return false;
})
.filter(Boolean) as any[];
emit('init', 'html');
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error parsing XML:', error);
}
}
/** /**
* 解析xml * 解析xml
*/ */
@ -88,21 +175,15 @@
const parser = new DOMParser(); const parser = new DOMParser();
const xmlDoc = parser.parseFromString(props.xmlString, 'application/xml'); const xmlDoc = parser.parseFromString(props.xmlString, 'application/xml');
// parsererror XML // parsererror XML
const errors = xmlDoc.getElementsByTagName('parsererror'); const xmlErrors = xmlDoc.getElementsByTagName('parsererror');
if (errors.length > 0) { if (xmlErrors.length > 0) {
isValidXml.value = false; parseHtml();
return; return;
} }
isValidXml.value = true; isValidXml.value = true;
parsedXml.value = xmlDoc; parsedXml.value = xmlDoc;
// XML icon const beautifyDoc = new XmlBeautify().beautify(props.xmlString);
flattenedXml.value = new XmlBeautify() replaceDoc(beautifyDoc);
.beautify(props.xmlString)
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/(&lt;([^/][^&]*?)&gt;)/g, '<span style="color: rgb(var(--primary-5));cursor: pointer">$1📋</span>')
.split(/\r?\n/)
.map((e) => ({ content: e, xpath: '' }));
// XML xpath // XML xpath
flattenXml(xmlDoc.documentElement, ''); flattenXml(xmlDoc.documentElement, '');
// XML xpath xpath // XML xpath xpath
@ -118,6 +199,7 @@
} }
return e; return e;
}); });
emit('init', 'xml');
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('Error parsing XML:', error); console.error('Error parsing XML:', error);

View File

@ -71,10 +71,14 @@
<MsButton v-if="hasChange" @click="handleReset">{{ t('msTable.columnSetting.resetDefault') }}</MsButton> <MsButton v-if="hasChange" @click="handleReset">{{ t('msTable.columnSetting.resetDefault') }}</MsButton>
</div> </div>
<div class="flex-col"> <div class="flex-col">
<div v-for="item in nonSortColumn" :key="item.dataIndex" class="column-item"> <div
<div v-show="item.dataIndex !== 'operation'">{{ t((item.title || item.columnTitle) as string) }}</div> v-for="item in nonSortColumn"
<a-switch
v-show="item.dataIndex !== 'operation'" v-show="item.dataIndex !== 'operation'"
:key="item.dataIndex"
class="column-item"
>
<div>{{ t((item.title || item.columnTitle) as string) }}</div>
<a-switch
v-model="item.showInTable" v-model="item.showInTable"
size="small" size="small"
:disabled="item.columnSelectorDisabled" :disabled="item.columnSelectorDisabled"
@ -84,17 +88,17 @@
</div> </div>
</div> </div>
<a-divider orientation="center" class="non-sort" <a-divider orientation="center" class="non-sort"
><span class="one-line-text text-xs text-[var(--color-text-4)]">{{ ><span class="one-line-text text-xs text-[var(--color-text-4)]">
t('msTable.columnSetting.nonSort') {{ t('msTable.columnSetting.nonSort') }}
}}</span></a-divider </span></a-divider
> >
<VueDraggable v-model="couldSortColumn" handle=".sort-handle" ghost-class="ghost" @change="handleSwitchChange"> <VueDraggable v-model="couldSortColumn" handle=".sort-handle" ghost-class="ghost" @change="handleSwitchChange">
<div v-for="element in couldSortColumn" :key="element.dataIndex" class="column-drag-item"> <div v-for="element in couldSortColumn" :key="element.dataIndex" class="column-drag-item">
<div class="flex w-[60%] items-center"> <div class="flex w-[60%] items-center">
<MsIcon type="icon-icon_drag" class="sort-handle cursor-move text-[16px] text-[var(--color-text-4)]" /> <MsIcon type="icon-icon_drag" class="sort-handle cursor-move text-[16px] text-[var(--color-text-4)]" />
<span class="one-line-text ml-[8px] max-w-[85%]">{{ <span class="one-line-text ml-[8px] max-w-[85%]">
t((element.title || element.columnTitle) as string) {{ t((element.title || element.columnTitle) as string) }}
}}</span> </span>
</div> </div>
<a-switch v-model="element.showInTable" size="small" type="line" @change="handleSwitchChange" /> <a-switch v-model="element.showInTable" size="small" type="line" @change="handleSwitchChange" />
</div> </div>

View File

@ -93,7 +93,7 @@
</a-tooltip> </a-tooltip>
</li> </li>
<li> <li>
<a-dropdown trigger="click" position="br"> <a-dropdown trigger="click" position="br" @select="handleHelpSelect">
<a-tooltip :content="t('settings.navbar.help')"> <a-tooltip :content="t('settings.navbar.help')">
<a-button type="secondary"> <a-button type="secondary">
<template #icon> <template #icon>
@ -102,7 +102,7 @@
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<template #content> <template #content>
<a-doption value="doc"> <a-doption v-if="appStore.pageConfig.helpDoc" value="doc">
<component :is="IconQuestionCircle"></component> <component :is="IconQuestionCircle"></component>
{{ t('settings.help.doc') }} {{ t('settings.help.doc') }}
</a-doption> </a-doption>
@ -184,7 +184,6 @@
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const unReadCount = ref<number>(0); const unReadCount = ref<number>(0);
async function checkMessageRead() { async function checkMessageRead() {
@ -213,7 +212,8 @@
const showProjectSelect = computed(() => { const showProjectSelect = computed(() => {
const { getRouteLevelByKey } = usePathMap(); const { getRouteLevelByKey } = usePathMap();
// //
return getRouteLevelByKey(route.name as PathMapRoute) === MENU_LEVEL[2]; const level = getRouteLevelByKey(route.name as PathMapRoute);
return level === MENU_LEVEL[2] || level === null;
}); });
async function selectProject( async function selectProject(
@ -266,6 +266,12 @@
messageCenterVisible.value = true; messageCenterVisible.value = true;
} }
function handleHelpSelect(val: string | number | Record<string, any> | undefined) {
if (val === 'doc') {
window.open(appStore.pageConfig.helpDoc, '_blank');
}
}
onMounted(() => { onMounted(() => {
if (route.query.task) { if (route.query.task) {
goTaskCenter(); goTaskCenter();

View File

@ -208,7 +208,7 @@ export const pathMap: PathMapItem[] = [
level: MENU_LEVEL[2], level: MENU_LEVEL[2],
}, },
{ {
key: 'CASE_MANAGEMENT_CASE_DETAIL', // 功能测试-功能用例-用例评审 key: 'CASE_MANAGEMENT_CASE_DETAIL', // 功能测试-功能用例-用例详情
locale: 'menu.caseManagement.featureCaseDetail', locale: 'menu.caseManagement.featureCaseDetail',
route: RouteEnum.CASE_MANAGEMENT_CASE_DETAIL, route: RouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
permission: [], permission: [],
@ -224,7 +224,7 @@ export const pathMap: PathMapItem[] = [
{ {
key: 'CASE_MANAGEMENT_REVIEW_DETAIL', // 功能测试-功能用例-用例评审 key: 'CASE_MANAGEMENT_REVIEW_DETAIL', // 功能测试-功能用例-用例评审
locale: 'menu.caseManagement.caseManagementReviewDetail', locale: 'menu.caseManagement.caseManagementReviewDetail',
route: RouteEnum.CASE_MANAGEMENT_REVIEW_CREATE, route: RouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL,
permission: [], permission: [],
level: MENU_LEVEL[2], level: MENU_LEVEL[2],
}, },

View File

@ -39,7 +39,7 @@ const defaultLoginConfig = {
const defaultPlatformConfig = { const defaultPlatformConfig = {
logoPlatform: [], logoPlatform: [],
platformName: 'MeterSphere', platformName: 'MeterSphere',
helpDoc: '', helpDoc: 'https://metersphere.io/docs/v3.x/',
}; };
const useAppStore = defineStore('app', { const useAppStore = defineStore('app', {
@ -105,8 +105,10 @@ const useAppStore = defineStore('app', {
...state.defaultPlatformConfig, ...state.defaultPlatformConfig,
}; };
}, },
getCurrentEnvId(state: AppState): string {
return state.currentEnvConfig?.id || '';
},
}, },
actions: { actions: {
/** /**
* *

View File

@ -1,4 +1,4 @@
import { DOMParser } from '@xmldom/xmldom'; import { DOMParser as XmlDOMParser } from '@xmldom/xmldom';
import * as xpath from 'xpath'; import * as xpath from 'xpath';
/** /**
@ -10,7 +10,7 @@ import * as xpath from 'xpath';
export function matchXMLWithXPath(xmlText: string, xpathQuery: string): xpath.SelectReturnType { export function matchXMLWithXPath(xmlText: string, xpathQuery: string): xpath.SelectReturnType {
try { try {
// 解析 XML 文本 // 解析 XML 文本
const xmlDoc = new DOMParser().parseFromString(xmlText, 'text/xml'); const xmlDoc = new XmlDOMParser().parseFromString(xmlText, 'text/xml');
// 创建一个命名空间解析器 // 创建一个命名空间解析器
const resolver = (prefix: string) => { const resolver = (prefix: string) => {
@ -31,4 +31,33 @@ export function matchXMLWithXPath(xmlText: string, xpathQuery: string): xpath.Se
} }
} }
export default { matchXMLWithXPath }; /**
* xpath html
* @param htmlText html文本
* @param xpathQuery xpath
* @returns
*/
export function extractTextFromHtmlWithXPath(htmlText: string, xpathQuery: string): Node[] {
try {
// 解析 HTML 文本
const parser = new DOMParser();
const htmlDoc = parser.parseFromString(htmlText, 'text/html');
// 使用 XPath 查询匹配的节点
const iterator = document.evaluate(xpathQuery, htmlDoc.documentElement, null, XPathResult.ANY_TYPE, null);
// 提取匹配节点的文本内容
let node = iterator.iterateNext();
const nodes: Node[] = [];
while (node) {
nodes.push(node);
node = iterator.iterateNext();
}
// 返回匹配节点
return nodes;
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error parsing HTML or executing XPath query:', error);
return [];
}
}

View File

@ -749,6 +749,7 @@ if (!result){
}, },
{ {
title: '', title: '',
dataIndex: 'operation',
slotName: 'operation', slotName: 'operation',
width: 50, width: 50,
}, },
@ -801,7 +802,7 @@ if (!result){
value: RequestExtractEnvType.TEMPORARY, value: RequestExtractEnvType.TEMPORARY,
}, },
], ],
width: 130, width: 150,
}, },
{ {
title: 'apiTestDebug.mode', title: 'apiTestDebug.mode',
@ -872,6 +873,7 @@ if (!result){
{ {
title: '', title: '',
slotName: 'operation', slotName: 'operation',
dataIndex: 'operation',
fixed: 'right', fixed: 'right',
moreAction: [ moreAction: [
{ {

View File

@ -24,7 +24,7 @@
<MsJsonPathPicker :data="props.response || ''" class="bg-white" @init="initJsonPath" @pick="handlePathPick" /> <MsJsonPathPicker :data="props.response || ''" class="bg-white" @init="initJsonPath" @pick="handlePathPick" />
</div> </div>
<div v-else-if="expressionForm.extractType === RequestExtractExpressionEnum.X_PATH" class="code-container"> <div v-else-if="expressionForm.extractType === RequestExtractExpressionEnum.X_PATH" class="code-container">
<MsXPathPicker :xml-string="props.response || ''" class="bg-white" @pick="handlePathPick" /> <MsXPathPicker :xml-string="props.response || ''" class="bg-white" @init="initXpath" @pick="handlePathPick" />
</div> </div>
<a-form ref="expressionFormRef" :model="expressionForm" layout="vertical" class="mt-[16px]"> <a-form ref="expressionFormRef" :model="expressionForm" layout="vertical" class="mt-[16px]">
<a-form-item <a-form-item
@ -162,7 +162,7 @@
import moreSetting from './moreSetting.vue'; import moreSetting from './moreSetting.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { matchXMLWithXPath } from '@/utils/xpath'; import { extractTextFromHtmlWithXPath, matchXMLWithXPath } from '@/utils/xpath';
import type { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/common'; import type { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/common';
import { RequestExtractExpressionEnum, RequestExtractExpressionRuleType } from '@/enums/apiEnum'; import { RequestExtractExpressionEnum, RequestExtractExpressionRuleType } from '@/enums/apiEnum';
@ -193,6 +193,7 @@
const expressionForm = ref({ ...props.config }); const expressionForm = ref({ ...props.config });
const expressionFormRef = ref<FormInstance | null>(null); const expressionFormRef = ref<FormInstance | null>(null);
const parseJson = ref<string | Record<string, any>>({}); const parseJson = ref<string | Record<string, any>>({});
const isHtml = ref(false);
const matchResult = ref<any[]>([]); // const matchResult = ref<any[]>([]); //
const isMatched = ref(false); // const isMatched = ref(false); //
@ -211,6 +212,10 @@
parseJson.value = _parseJson; parseJson.value = _parseJson;
} }
function initXpath(type: 'xml' | 'html') {
isHtml.value = type === 'html';
}
function handlePathPick(path: string, _parseJson: string | Record<string, any>) { function handlePathPick(path: string, _parseJson: string | Record<string, any>) {
expressionForm.value.expression = path; expressionForm.value.expression = path;
parseJson.value = _parseJson; parseJson.value = _parseJson;
@ -223,7 +228,9 @@
function testExpression() { function testExpression() {
switch (props.config.extractType) { switch (props.config.extractType) {
case RequestExtractExpressionEnum.X_PATH: case RequestExtractExpressionEnum.X_PATH:
const nodes = matchXMLWithXPath(props.response || '', expressionForm.value.expression); const nodes = isHtml.value
? extractTextFromHtmlWithXPath(props.response || '', expressionForm.value.expression)
: matchXMLWithXPath(props.response || '', expressionForm.value.expression);
if (nodes) { if (nodes) {
// //
if (typeof nodes === 'boolean' || typeof nodes === 'string' || typeof nodes === 'number') { if (typeof nodes === 'boolean' || typeof nodes === 'string' || typeof nodes === 'number') {
@ -248,7 +255,7 @@
JSONPath({ JSONPath({
json: parseJson.value, json: parseJson.value,
path: expressionForm.value.expression, path: expressionForm.value.expression,
})?.map((e) => JSON.stringify(e).replace(/Number\(([^)]+)\)/g, '$1')) || []; })?.map((e) => JSON.stringify(e).replace(/"Number\(([^)]+)\)"|Number\(([^)]+)\)/g, '$1$2')) || [];
} catch (error) { } catch (error) {
matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression }) || []; matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression }) || [];
} }

View File

@ -244,6 +244,7 @@
: [ : [
{ {
title: '', title: '',
dataIndex: 'operation',
slotName: 'operation', slotName: 'operation',
fixed: 'right' as TableColumnData['fixed'], fixed: 'right' as TableColumnData['fixed'],
format: innerParams.value.bodyType, format: innerParams.value.bodyType,

View File

@ -77,6 +77,7 @@
: [ : [
{ {
title: '', title: '',
dataIndex: 'operation',
slotName: 'operation', slotName: 'operation',
width: 50, width: 50,
}, },

View File

@ -112,6 +112,7 @@
: [ : [
{ {
title: '', title: '',
dataIndex: 'operation',
slotName: 'operation', slotName: 'operation',
fixed: 'right' as TableColumnData['fixed'], fixed: 'right' as TableColumnData['fixed'],
width: 50, width: 50,

View File

@ -113,6 +113,7 @@
: [ : [
{ {
title: '', title: '',
dataIndex: 'operation',
slotName: 'operation', slotName: 'operation',
fixed: 'right' as TableColumnData['fixed'], fixed: 'right' as TableColumnData['fixed'],
width: 50, width: 50,

View File

@ -508,6 +508,7 @@
showSelectAll: !props.readOnly, showSelectAll: !props.readOnly,
draggable: hasAnyPermission(['PROJECT_API_DEFINITION:READ+UPDATE']) ? { type: 'handle', width: 32 } : undefined, draggable: hasAnyPermission(['PROJECT_API_DEFINITION:READ+UPDATE']) ? { type: 'handle', width: 32 } : undefined,
heightUsed: 256, heightUsed: 256,
paginationSize: 'mini',
showSubdirectory: true, showSubdirectory: true,
}, },
(item) => ({ (item) => ({
@ -688,7 +689,14 @@
selectIds, selectIds,
selectAll: !!params?.selectAll, selectAll: !!params?.selectAll,
excludeIds: params?.excludeIds || [], excludeIds: params?.excludeIds || [],
condition: { keyword: keyword.value }, condition: {
keyword: keyword.value,
filter: {
status: statusFilters.value,
method: methodFilters.value,
createUser: createUserFilters.value,
},
},
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
moduleIds: await getModuleIds(), moduleIds: await getModuleIds(),
deleteAll: true, deleteAll: true,
@ -813,6 +821,7 @@
filter: { filter: {
status: statusFilters.value, status: statusFilters.value,
method: methodFilters.value, method: methodFilters.value,
createUser: createUserFilters.value,
}, },
}, },
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
@ -857,6 +866,7 @@
filter: { filter: {
status: statusFilters.value, status: statusFilters.value,
method: methodFilters.value, method: methodFilters.value,
createUser: createUserFilters.value,
}, },
}, },
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,

View File

@ -404,7 +404,6 @@
updateCasePriority, updateCasePriority,
updateCaseStatus, updateCaseStatus,
} from '@/api/modules/api-test/management'; } from '@/api/modules/api-test/management';
import { getProjectOptions } from '@/api/modules/project-management/projectMember';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import useTableStore from '@/hooks/useTableStore'; import useTableStore from '@/hooks/useTableStore';
@ -774,6 +773,7 @@
status: statusFilters.value, status: statusFilters.value,
priority: caseFilters.value, priority: caseFilters.value,
lastReportStatus: lastReportStatusFilters.value, lastReportStatus: lastReportStatusFilters.value,
createUser: createUserFilters.value,
}, },
}, },
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,

View File

@ -50,7 +50,7 @@
</div> </div>
<a-divider class="my-[8px]" /> <a-divider class="my-[8px]" />
<a-spin class="w-full" :style="{ height: `calc(100vh - 320px)` }" :loading="loading"> <a-spin class="w-full" :style="{ height: `calc(100vh - 300px)` }" :loading="loading">
<MsTree <MsTree
v-model:focus-node-key="focusNodeKey" v-model:focus-node-key="focusNodeKey"
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
@ -186,7 +186,7 @@
}; };
} }
return { return {
height: 'calc(100vh - 325px)', height: 'calc(100vh - 300px)',
threshold: 200, threshold: 200,
fixedSize: true, fixedSize: true,
buffer: 15, // 10 padding buffer: 15, // 10 padding

View File

@ -1339,7 +1339,16 @@
selectIds: batchParams.value?.selectedIds || [], selectIds: batchParams.value?.selectedIds || [],
selectAll: !!batchParams.value?.selectAll, selectAll: !!batchParams.value?.selectAll,
excludeIds: batchParams.value?.excludeIds || [], excludeIds: batchParams.value?.excludeIds || [],
condition: { keyword: keyword.value }, condition: {
keyword: keyword.value,
filter: {
lastReportStatus: lastReportStatusListFilters.value,
status: statusFilters.value,
priority: priorityFilters.value,
createUser: createUserFilters.value,
updateUser: updateUserFilters.value,
},
},
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
moduleIds: props.activeModule === 'all' ? [] : [props.activeModule], moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
type: batchForm.value?.attr, type: batchForm.value?.attr,
@ -1399,7 +1408,16 @@
selectIds: batchParams.value?.selectedIds || [], selectIds: batchParams.value?.selectedIds || [],
selectAll: !!batchParams.value?.selectAll, selectAll: !!batchParams.value?.selectAll,
excludeIds: batchParams.value?.excludeIds || [], excludeIds: batchParams.value?.excludeIds || [],
condition: { keyword: keyword.value }, condition: {
keyword: keyword.value,
filter: {
lastReportStatus: lastReportStatusListFilters.value,
status: statusFilters.value,
priority: priorityFilters.value,
createUser: createUserFilters.value,
updateUser: updateUserFilters.value,
},
},
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
moduleIds: props.activeModule === 'all' ? [] : [props.activeModule], moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
targetModuleId: selectedBatchOptModuleKey.value, targetModuleId: selectedBatchOptModuleKey.value,

View File

@ -251,7 +251,7 @@
res = await executeScenario({ res = await executeScenario({
id: activeScenarioTab.value.id, id: activeScenarioTab.value.id,
grouped: false, grouped: false,
environmentId: activeScenarioTab.value.environmentId || '', environmentId: appStore.getCurrentEnvId || '',
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
scenarioConfig: activeScenarioTab.value.scenarioConfig, scenarioConfig: activeScenarioTab.value.scenarioConfig,
...executeParams, ...executeParams,
@ -267,7 +267,7 @@
res = await debugScenario({ res = await debugScenario({
id: activeScenarioTab.value.id, id: activeScenarioTab.value.id,
grouped: false, grouped: false,
environmentId: activeScenarioTab.value.environmentId || '', environmentId: appStore.getCurrentEnvId || '',
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
scenarioConfig: activeScenarioTab.value.scenarioConfig, scenarioConfig: activeScenarioTab.value.scenarioConfig,
stepFileParam: activeScenarioTab.value.stepFileParam, stepFileParam: activeScenarioTab.value.stepFileParam,
@ -437,7 +437,7 @@
scenarioTabs.value.push({ scenarioTabs.value.push({
...cloneDeep(defaultScenario), ...cloneDeep(defaultScenario),
id: getGenerateId(), id: getGenerateId(),
environmentId: appStore.currentEnvConfig?.id || '', environmentId: appStore.getCurrentEnvId || '',
label: `${t('apiScenario.createScenario')}${scenarioTabs.value.length}`, label: `${t('apiScenario.createScenario')}${scenarioTabs.value.length}`,
moduleId: activeModule.value === 'all' ? 'root' : activeModule.value, moduleId: activeModule.value === 'all' ? 'root' : activeModule.value,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
@ -494,7 +494,7 @@
}; };
}), }),
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
environmentId: activeScenarioTab.value.environmentId || '', environmentId: appStore.getCurrentEnvId || '',
}); });
const scenarioDetail = await getScenarioDetail(res.id); const scenarioDetail = await getScenarioDetail(res.id);
scenarioDetail.stepDetails = {}; scenarioDetail.stepDetails = {};
@ -534,7 +534,7 @@
} else { } else {
await updateScenario({ await updateScenario({
...activeScenarioTab.value, ...activeScenarioTab.value,
environmentId: activeScenarioTab.value.environmentId || '', environmentId: appStore.getCurrentEnvId || '',
steps: mapTree(activeScenarioTab.value.steps, (node) => { steps: mapTree(activeScenarioTab.value.steps, (node) => {
return { return {
...node, ...node,

View File

@ -31,7 +31,12 @@
> >
<!-- ID --> <!-- ID -->
<template #num="{ record, rowIndex }"> <template #num="{ record, rowIndex }">
<a-button type="text" class="px-0" size="mini" @click="handleShowDetail(record.id, rowIndex)"> <a-button
type="text"
class="px-0 text-[14px] leading-[22px]"
size="mini"
@click="handleShowDetail(record.id, rowIndex)"
>
{{ record.num }} {{ record.num }}
</a-button> </a-button>
</template> </template>

View File

@ -51,7 +51,7 @@
<template #num="{ record, rowIndex }"> <template #num="{ record, rowIndex }">
<span <span
type="text" type="text"
class="one-line-text px-0 text-[rgb(var(--primary-5))]" class="one-line-text cursor-pointer px-0 text-[rgb(var(--primary-5))]"
@click="showCaseDetail(record.id, rowIndex)" @click="showCaseDetail(record.id, rowIndex)"
>{{ record.num }}</span >{{ record.num }}</span
> >

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="px-[24px] py-[16px]"> <div class="px-[24px] py-[8px]">
<div class="mb-[16px] flex flex-wrap items-center justify-end"> <div class="mb-[8px] flex flex-wrap items-center justify-end">
<MsAdvanceFilter <MsAdvanceFilter
v-model:keyword="keyword" v-model:keyword="keyword"
:filter-config-list="filterConfigList" :filter-config-list="filterConfigList"
@ -73,7 +73,7 @@
</template> </template>
<template #num="{ record }"> <template #num="{ record }">
<a-tooltip :content="record.num"> <a-tooltip :content="record.num">
<a-button type="text" class="px-0" @click="review(record)"> <a-button type="text" class="px-0 !text-[14px] !leading-[22px]" size="mini" @click="review(record)">
<div class="one-line-text max-w-[168px]">{{ record.num }}</div> <div class="one-line-text max-w-[168px]">{{ record.num }}</div>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -434,10 +434,11 @@
{ {
scroll: { x: '100%' }, scroll: { x: '100%' },
tableKey: TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE, tableKey: TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE,
heightUsed: 472, heightUsed: 372,
showSetting: true, showSetting: true,
selectable: true, selectable: true,
showSelectAll: true, showSelectAll: true,
paginationSize: 'mini',
draggable: { type: 'handle', width: 32 }, draggable: { type: 'handle', width: 32 },
}, },
(record) => { (record) => {
@ -628,6 +629,7 @@
userId: props.onlyMine ? userStore.id || '' : '', userId: props.onlyMine ? userStore.id || '' : '',
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds], moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
...batchParams.value, ...batchParams.value,
...tableParams.value,
}); });
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
resetSelector(); resetSelector();
@ -663,6 +665,7 @@
notifier: dialogForm.value.commentIds.join(';'), notifier: dialogForm.value.commentIds.join(';'),
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds], moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
...batchParams.value, ...batchParams.value,
...tableParams.value,
}); });
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
dialogVisible.value = false; dialogVisible.value = false;
@ -690,6 +693,7 @@
append: dialogForm.value.isAppend, // append: dialogForm.value.isAppend, //
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds], moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
...batchParams.value, ...batchParams.value,
...tableParams.value,
}); });
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
dialogVisible.value = false; dialogVisible.value = false;
@ -720,6 +724,7 @@
notifier: dialogForm.value.commentIds.join(';'), notifier: dialogForm.value.commentIds.join(';'),
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds], moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
...batchParams.value, ...batchParams.value,
...tableParams.value,
}); });
Message.success(t('caseManagement.caseReview.reviewSuccess')); Message.success(t('caseManagement.caseReview.reviewSuccess'));
dialogVisible.value = false; dialogVisible.value = false;
@ -890,4 +895,5 @@
:deep(.arco-radio-label) { :deep(.arco-radio-label) {
@apply inline-flex; @apply inline-flex;
} }
.ms-table--special-small();
</style> </style>

View File

@ -4,7 +4,7 @@
v-model:model-value="moduleKeyword" v-model:model-value="moduleKeyword"
:placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')" :placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')"
allow-clear allow-clear
class="mb-[16px]" class="mb-[8px]"
:max-length="255" :max-length="255"
/> />
<div class="folder"> <div class="folder">

View File

@ -67,8 +67,7 @@
<template v-if="!props.isModal" #extra="nodeData"> <template v-if="!props.isModal" #extra="nodeData">
<!-- 默认模块的 id 是root默认模块不可编辑不可添加子模块 --> <!-- 默认模块的 id 是root默认模块不可编辑不可添加子模块 -->
<popConfirm <popConfirm
v-if="nodeData.id !== 'root'" v-if="nodeData.id !== 'root' && hasAnyPermission(['CASE_REVIEW:READ+DELETE'])"
v-permission="['CASE_REVIEW:READ+ADD']"
mode="add" mode="add"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')" :all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
:parent-id="nodeData.id" :parent-id="nodeData.id"
@ -80,8 +79,7 @@
</MsButton> </MsButton>
</popConfirm> </popConfirm>
<popConfirm <popConfirm
v-if="nodeData.id !== 'root'" v-if="nodeData.id !== 'root' && hasAnyPermission(['CASE_REVIEW:READ+UPDATE'])"
v-permission="['CASE_REVIEW:READ+UPDATE']"
mode="rename" mode="rename"
:parent-id="nodeData.id" :parent-id="nodeData.id"
:node-id="nodeData.id" :node-id="nodeData.id"

View File

@ -92,7 +92,7 @@
</template> </template>
<template #num="{ record }"> <template #num="{ record }">
<a-tooltip :content="`${record.num}`"> <a-tooltip :content="`${record.num}`">
<a-button type="text" class="px-0" size="mini" @click="openDetail(record.id)"> <a-button type="text" class="px-0 !text-[14px] !leading-[22px]" size="mini" @click="openDetail(record.id)">
<div class="one-line-text max-w-[168px]">{{ record.num }}</div> <div class="one-line-text max-w-[168px]">{{ record.num }}</div>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -675,14 +675,7 @@
try { try {
batchMoveFileLoading.value = true; batchMoveFileLoading.value = true;
await moveReview({ await moveReview({
selectIds: batchParams.value?.selectedIds || [], ...tableQueryParams.value,
selectAll: !!batchParams.value?.selectAll,
excludeIds: batchParams.value?.excludeIds || [],
currentSelectCount: batchParams.value?.currentSelectCount || 0,
condition: { keyword: keyword.value },
projectId: appStore.currentProjectId,
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder],
moveModuleId: selectedModuleKeys.value[0],
}); });
Message.success(t('caseManagement.caseReview.batchMoveSuccess')); Message.success(t('caseManagement.caseReview.batchMoveSuccess'));
loadList(); loadList();

View File

@ -102,7 +102,7 @@
<MsCard class="mt-[16px]" :special-height="180" simple has-breadcrumb no-content-padding> <MsCard class="mt-[16px]" :special-height="180" simple has-breadcrumb no-content-padding>
<MsSplitBox> <MsSplitBox>
<template #first> <template #first>
<div class="p-[24px]"> <div class="p-[16px]">
<CaseTree <CaseTree
ref="folderTreeRef" ref="folderTreeRef"
:modules-count="modulesCount" :modules-count="modulesCount"