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-plugin-persistedstate": "^3.2.0",
"pm": "link:@/tiptap/pm",
"pretty": "^2.0.0",
"query-string": "^8.1.0",
"resize-observer-polyfill": "^1.5.1",
"sortablejs": "^1.15.0",

View File

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

View File

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

View File

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

View File

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

View File

@ -13,12 +13,13 @@
import { useI18n } from '@/hooks/useI18n';
import { XpathNode } from './types';
import HtmlBeautify from 'pretty';
import XmlBeautify from 'xml-beautify';
const props = defineProps<{
xmlString: string;
}>();
const emit = defineEmits(['pick']);
const emit = defineEmits(['pick', 'init']);
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) {
if (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
*/
@ -88,21 +175,15 @@
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(props.xmlString, 'application/xml');
// parsererror XML
const errors = xmlDoc.getElementsByTagName('parsererror');
if (errors.length > 0) {
isValidXml.value = false;
const xmlErrors = xmlDoc.getElementsByTagName('parsererror');
if (xmlErrors.length > 0) {
parseHtml();
return;
}
isValidXml.value = true;
parsedXml.value = xmlDoc;
// XML icon
flattenedXml.value = new XmlBeautify()
.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: '' }));
const beautifyDoc = new XmlBeautify().beautify(props.xmlString);
replaceDoc(beautifyDoc);
// XML xpath
flattenXml(xmlDoc.documentElement, '');
// XML xpath xpath
@ -118,6 +199,7 @@
}
return e;
});
emit('init', 'xml');
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error parsing XML:', error);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { DOMParser } from '@xmldom/xmldom';
import { DOMParser as XmlDOMParser } from '@xmldom/xmldom';
import * as xpath from 'xpath';
/**
@ -10,7 +10,7 @@ import * as xpath from 'xpath';
export function matchXMLWithXPath(xmlText: string, xpathQuery: string): xpath.SelectReturnType {
try {
// 解析 XML 文本
const xmlDoc = new DOMParser().parseFromString(xmlText, 'text/xml');
const xmlDoc = new XmlDOMParser().parseFromString(xmlText, 'text/xml');
// 创建一个命名空间解析器
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: '',
dataIndex: 'operation',
slotName: 'operation',
width: 50,
},
@ -801,7 +802,7 @@ if (!result){
value: RequestExtractEnvType.TEMPORARY,
},
],
width: 130,
width: 150,
},
{
title: 'apiTestDebug.mode',
@ -872,6 +873,7 @@ if (!result){
{
title: '',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
moreAction: [
{

View File

@ -24,7 +24,7 @@
<MsJsonPathPicker :data="props.response || ''" class="bg-white" @init="initJsonPath" @pick="handlePathPick" />
</div>
<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>
<a-form ref="expressionFormRef" :model="expressionForm" layout="vertical" class="mt-[16px]">
<a-form-item
@ -162,7 +162,7 @@
import moreSetting from './moreSetting.vue';
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 { RequestExtractExpressionEnum, RequestExtractExpressionRuleType } from '@/enums/apiEnum';
@ -193,6 +193,7 @@
const expressionForm = ref({ ...props.config });
const expressionFormRef = ref<FormInstance | null>(null);
const parseJson = ref<string | Record<string, any>>({});
const isHtml = ref(false);
const matchResult = ref<any[]>([]); //
const isMatched = ref(false); //
@ -211,6 +212,10 @@
parseJson.value = _parseJson;
}
function initXpath(type: 'xml' | 'html') {
isHtml.value = type === 'html';
}
function handlePathPick(path: string, _parseJson: string | Record<string, any>) {
expressionForm.value.expression = path;
parseJson.value = _parseJson;
@ -223,7 +228,9 @@
function testExpression() {
switch (props.config.extractType) {
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 (typeof nodes === 'boolean' || typeof nodes === 'string' || typeof nodes === 'number') {
@ -248,7 +255,7 @@
JSONPath({
json: parseJson.value,
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) {
matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression }) || [];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@
</div>
<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
v-model:focus-node-key="focusNodeKey"
v-model:selected-keys="selectedKeys"
@ -186,7 +186,7 @@
};
}
return {
height: 'calc(100vh - 325px)',
height: 'calc(100vh - 300px)',
threshold: 200,
fixedSize: true,
buffer: 15, // 10 padding

View File

@ -1339,7 +1339,16 @@
selectIds: batchParams.value?.selectedIds || [],
selectAll: !!batchParams.value?.selectAll,
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,
moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
type: batchForm.value?.attr,
@ -1399,7 +1408,16 @@
selectIds: batchParams.value?.selectedIds || [],
selectAll: !!batchParams.value?.selectAll,
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,
moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
targetModuleId: selectedBatchOptModuleKey.value,

View File

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

View File

@ -31,7 +31,12 @@
>
<!-- ID -->
<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 }}
</a-button>
</template>

View File

@ -51,7 +51,7 @@
<template #num="{ record, rowIndex }">
<span
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)"
>{{ record.num }}</span
>

View File

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

View File

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

View File

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

View File

@ -92,7 +92,7 @@
</template>
<template #num="{ record }">
<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>
</a-button>
</a-tooltip>
@ -675,14 +675,7 @@
try {
batchMoveFileLoading.value = true;
await moveReview({
selectIds: batchParams.value?.selectedIds || [],
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],
...tableQueryParams.value,
});
Message.success(t('caseManagement.caseReview.batchMoveSuccess'));
loadList();

View File

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