fix(缺陷管理): 关联用例部分问题

--bug=1036504 --user=宋昌昌 【缺陷管理】缺陷详情-关联用例-列表有数据不展示-无法关联 https://www.tapd.cn/55049933/s/1468978
This commit is contained in:
song-cc-rock 2024-03-04 20:46:07 +08:00 committed by 刘瑞斌
parent abcdecb5e2
commit dbacfc7d7c
11 changed files with 109 additions and 61 deletions

View File

@ -9,6 +9,9 @@ public class BugRelateCaseDTO{
@Schema(description = "关联ID") @Schema(description = "关联ID")
private String relateId; private String relateId;
@Schema(description = "关联用例ID")
private String relateCaseId;
@Schema(description = "关联用例名称") @Schema(description = "关联用例名称")
private String relateCaseName; private String relateCaseName;

View File

@ -11,7 +11,6 @@
from functional_case_module fcm left join functional_case fc on fc.module_id = fcm.id from functional_case_module fcm left join functional_case fc on fc.module_id = fcm.id
where fc.deleted = #{deleted} where fc.deleted = #{deleted}
and fc.project_id = #{request.projectId} and fc.project_id = #{request.projectId}
and fc.version_id = #{request.versionId}
and fc.id not in and fc.id not in
( (
select brc.case_id from bug_relation_case brc where brc.bug_id = #{request.sourceId} and brc.case_type = #{request.sourceType} select brc.case_id from bug_relation_case brc where brc.bug_id = #{request.sourceId} and brc.case_type = #{request.sourceType}
@ -46,7 +45,7 @@
</select> </select>
<select id="list" resultType="io.metersphere.bug.dto.response.BugRelateCaseDTO"> <select id="list" resultType="io.metersphere.bug.dto.response.BugRelateCaseDTO">
select fc.num relateId, fc.name relateCaseName, fc.project_id projectId, fc.version_id versionId, brc.case_type relateCaseType, select brc.id relateId, fc.num relateCaseId, fc.name relateCaseName, fc.project_id projectId, fc.version_id versionId, brc.case_type relateCaseType,
brc.test_plan_id is not null relatePlanCase, brc.case_id is not null relateCase brc.test_plan_id is not null relatePlanCase, brc.case_id is not null relateCase
from bug_relation_case brc join functional_case fc on (brc.case_id = fc.id or brc.test_plan_case_id = fc.id) from bug_relation_case brc join functional_case fc on (brc.case_id = fc.id or brc.test_plan_case_id = fc.id)
where brc.bug_id = #{request.bugId} and fc.deleted = false where brc.bug_id = #{request.bugId} and fc.deleted = false

View File

@ -713,7 +713,6 @@
from functional_case fc left join project_version pv ON fc.version_id = pv.id from functional_case fc left join project_version pv ON fc.version_id = pv.id
where fc.deleted = #{deleted} where fc.deleted = #{deleted}
and fc.project_id = #{request.projectId} and fc.project_id = #{request.projectId}
and fc.version_id = #{request.versionId}
and fc.id not in and fc.id not in
( (
select brc.case_id from bug_relation_case brc where brc.bug_id = #{request.sourceId} and brc.case_type = #{request.sourceType} select brc.case_id from bug_relation_case brc where brc.bug_id = #{request.sourceId} and brc.case_type = #{request.sourceType}

View File

@ -1,25 +1,45 @@
package io.metersphere.functional.provider; package io.metersphere.functional.provider;
import io.metersphere.dto.TestCaseProviderDTO; import io.metersphere.dto.TestCaseProviderDTO;
import io.metersphere.functional.dto.FunctionalCaseCustomFieldDTO;
import io.metersphere.functional.mapper.ExtFunctionalCaseMapper; import io.metersphere.functional.mapper.ExtFunctionalCaseMapper;
import io.metersphere.functional.service.FunctionalCaseService;
import io.metersphere.provider.BaseAssociateCaseProvider; import io.metersphere.provider.BaseAssociateCaseProvider;
import io.metersphere.request.AssociateOtherCaseRequest; import io.metersphere.request.AssociateOtherCaseRequest;
import io.metersphere.request.TestCasePageProviderRequest; import io.metersphere.request.TestCasePageProviderRequest;
import io.metersphere.sdk.util.Translator;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service @Service
public class AssociateCaseProvider implements BaseAssociateCaseProvider { public class AssociateCaseProvider implements BaseAssociateCaseProvider {
@Resource
private FunctionalCaseService functionalCaseService;
@Resource @Resource
private ExtFunctionalCaseMapper extFunctionalCaseMapper; private ExtFunctionalCaseMapper extFunctionalCaseMapper;
@Override @Override
public List<TestCaseProviderDTO> listUnRelatedTestCaseList(TestCasePageProviderRequest testCasePageProviderRequest) { public List<TestCaseProviderDTO> listUnRelatedTestCaseList(TestCasePageProviderRequest testCasePageProviderRequest) {
return extFunctionalCaseMapper.listUnRelatedCaseWithBug(testCasePageProviderRequest, false, testCasePageProviderRequest.getSortString()); List<TestCaseProviderDTO> functionalCases = extFunctionalCaseMapper.listUnRelatedCaseWithBug(testCasePageProviderRequest, false, testCasePageProviderRequest.getSortString());
if (CollectionUtils.isEmpty(functionalCases)) {
return new ArrayList<>();
}
List<String> ids = functionalCases.stream().map(TestCaseProviderDTO::getId).toList();
Map<String, List<FunctionalCaseCustomFieldDTO>> caseCustomFiledMap = functionalCaseService.getCaseCustomFiledMap(ids);
functionalCases.forEach(functionalCase -> {
List<FunctionalCaseCustomFieldDTO> customFields = caseCustomFiledMap.get(functionalCase.getId());
Optional<FunctionalCaseCustomFieldDTO> priorityField = customFields.stream().filter(field -> StringUtils.equals(Translator.get("custom_field.functional_priority"), field.getFieldName())).findFirst();
priorityField.ifPresent(functionalCaseCustomFieldDTO -> functionalCase.setPriority(functionalCaseCustomFieldDTO.getDefaultValue()));
});
return functionalCases;
} }
@Override @Override

View File

@ -62,7 +62,7 @@ export const getDemandListUrl = '/bug/case/page';
// 批量添加关联 // 批量添加关联
export const postAddDemandUrl = '/bug/case/relate'; export const postAddDemandUrl = '/bug/case/relate';
// 单个取消关联 // 单个取消关联
export const getCancelDemandUrl = '/bug/case/un-relate/'; export const getCancelDemandUrl = '/bug/case/un-relate';
// 未关联的用例列表 // 未关联的用例列表
export const getUnrelatedDemandListUrl = '/bug/case/un-relate/page'; export const getUnrelatedDemandListUrl = '/bug/case/un-relate/page';
// 未关联的模块树 // 未关联的模块树

View File

@ -112,7 +112,7 @@
</a-tooltip> </a-tooltip>
</template> </template>
<template #caseLevel="{ record }"> <template #caseLevel="{ record }">
<caseLevel :case-level="getCaseLevel(record)" /> <span>{{ t(record.priority) }}</span>
</template> </template>
</ms-base-table> </ms-base-table>
<div class="footer"> <div class="footer">
@ -141,35 +141,33 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue'; import {computed, ref, watch} from 'vue';
import { useRouter } from 'vue-router'; import {useRouter} from 'vue-router';
import { useVModel } from '@vueuse/core'; import {useVModel} from '@vueuse/core';
import { CustomTypeMaps, MsAdvanceFilter } from '@/components/pure/ms-advance-filter'; import {CustomTypeMaps, MsAdvanceFilter} from '@/components/pure/ms-advance-filter';
import { FilterFormItem, FilterType } from '@/components/pure/ms-advance-filter/type'; import {FilterFormItem, FilterType} from '@/components/pure/ms-advance-filter/type';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type'; import {MsTableColumn} from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import MsProjectSelect from '@/components/business/ms-project-select/index.vue'; import MsProjectSelect from '@/components/business/ms-project-select/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue'; import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import type {MsTreeNodeData} from '@/components/business/ms-tree/types';
import caseLevel from './caseLevel.vue'; import caseLevel from './caseLevel.vue';
import { getCustomFieldsTable } from '@/api/modules/case-management/featureCase'; import {getCustomFieldsTable} from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n'; import {useI18n} from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils'; import {mapTree} from '@/utils';
import type { CaseManagementTable } from '@/models/caseManagement/featureCase'; import type {CaseManagementTable} from '@/models/caseManagement/featureCase';
import type { CommonList, ModuleTreeNode, TableQueryParams } from '@/models/common'; import type {CommonList, ModuleTreeNode, TableQueryParams} from '@/models/common';
import { CaseManagementRouteEnum } from '@/enums/routeEnum'; import {CaseManagementRouteEnum} from '@/enums/routeEnum';
import {initGetModuleCountFunc, type RequestModuleEnum} from './utils';
import type { CaseLevel } from './types'; const router = useRouter();
import { initGetModuleCountFunc, type RequestModuleEnum } from './utils';
const router = useRouter();
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
@ -363,20 +361,7 @@
title: 'ms.case.associate.tags', title: 'ms.case.associate.tags',
dataIndex: 'tags', dataIndex: 'tags',
isTag: true, isTag: true,
}, }
{
title: 'caseManagement.featureCase.tableColumnCreateUser',
dataIndex: 'createUserName',
showInTable: true,
width: 300,
},
{
title: 'caseManagement.featureCase.tableColumnCreateTime',
slotName: 'createTime',
dataIndex: 'createTime',
showInTable: true,
width: 300,
},
]; ];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable( const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
@ -568,11 +553,6 @@
emit('save', params); emit('save', params);
} }
//
function getCaseLevel(record: CaseManagementTable) {
return (record.customFields.find((item: any) => item.name === '用例等级')?.value as CaseLevel) || 'P1';
}
function openDetail(id: string) { function openDetail(id: string) {
window.open( window.open(
`${window.location.origin}#${ `${window.location.origin}#${

View File

@ -36,6 +36,7 @@ export default {
'common.saveSuccess': 'Save success', 'common.saveSuccess': 'Save success',
'common.saveFailed': 'Save failed', 'common.saveFailed': 'Save failed',
'common.linkSuccess': 'Link success', 'common.linkSuccess': 'Link success',
'common.unLinkSuccess': 'Unlink success',
'common.confirmEnable': 'Confirm enable', 'common.confirmEnable': 'Confirm enable',
'common.confirmEnd': 'Confirm end', 'common.confirmEnd': 'Confirm end',
'common.confirmStart': 'Confirm start', 'common.confirmStart': 'Confirm start',
@ -122,4 +123,6 @@ export default {
'common.apply': 'Apply', 'common.apply': 'Apply',
'common.stop': 'Stop', 'common.stop': 'Stop',
'common.module': 'Module', 'common.module': 'Module',
'common.yes': 'Yes',
'common.no': 'No'
}; };

View File

@ -38,6 +38,7 @@ export default {
'common.saveSuccess': '保存成功', 'common.saveSuccess': '保存成功',
'common.saveFailed': '保存失败', 'common.saveFailed': '保存失败',
'common.linkSuccess': '关联成功', 'common.linkSuccess': '关联成功',
'common.unLinkSuccess': '取消关联成功',
'common.confirmEnable': '确认启用', 'common.confirmEnable': '确认启用',
'common.confirmDisable': '确认禁用', 'common.confirmDisable': '确认禁用',
'common.confirmClose': '确认关闭', 'common.confirmClose': '确认关闭',
@ -125,4 +126,6 @@ export default {
'common.apply': '应用', 'common.apply': '应用',
'common.stop': '停止', 'common.stop': '停止',
'common.module': '模块', 'common.module': '模块',
'common.yes': '是',
'common.no': '否'
}; };

View File

@ -13,7 +13,7 @@
</div> </div>
<ms-base-table v-bind="propsRes" v-on="propsEvent"> <ms-base-table v-bind="propsRes" v-on="propsEvent">
<template #defectName="{ record }"> <template #defectName="{ record }">
<span class="one-line-text max-w[300px]"> {{ record.name }}</span <span class="one-line-text max-w[300px]"> {{ record.relateCaseName }}</span
><span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span> ><span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
</template> </template>
<template #operation="{ record }"> <template #operation="{ record }">
@ -27,6 +27,29 @@
</MsButton> </MsButton>
</div> </div>
</template> </template>
<template #relatePlanTitle>
<div class="flex items-center">
<div class="font-medium text-[var(--color-text-3)]">
{{ t('bugManagement.detail.isPlanRelateCase') }}
</div>
<a-popover position="rt">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-3)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
<template #title>
<div class="w-[300px]"> {{ t('bugManagement.detail.isPlanRelateCaseTip1') }} </div>
<br>
<div class="w-[300px]"> {{ t('bugManagement.detail.isPlanRelateCaseTip2') }} </div>
<br>
<div class="w-[300px]"> {{ t('bugManagement.detail.isPlanRelateCaseTip3') }} </div>
</template>
</a-popover>
</div>
</template>
<template #isRelatePlanCase = "{ record }">
<span class="text-[var(--color-text-1)]">{{record.isRelatePlanCase ? t('common.yes') : t('common.no') }}</span>
</template>
</ms-base-table> </ms-base-table>
<MsCaseAssociate <MsCaseAssociate
v-model:visible="innerVisible" v-model:visible="innerVisible"
@ -98,7 +121,7 @@ const appStore = useAppStore();
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'caseManagement.featureCase.tableColumnID', title: 'caseManagement.featureCase.tableColumnID',
dataIndex: 'id', dataIndex: 'relateCaseId',
width: 200, width: 200,
showInTable: true, showInTable: true,
showTooltip: true, showTooltip: true,
@ -107,8 +130,17 @@ const appStore = useAppStore();
}, },
{ {
title: 'caseManagement.featureCase.tableColumnName', title: 'caseManagement.featureCase.tableColumnName',
slotName: 'name', dataIndex: 'relateCaseName',
dataIndex: 'name', showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'bugManagement.detail.isPlanRelateCase',
titleSlotName: 'relatePlanTitle',
slotName: 'isRelatePlanCase',
showInTable: true, showInTable: true,
showTooltip: true, showTooltip: true,
width: 300, width: 300,
@ -127,8 +159,7 @@ const appStore = useAppStore();
}, },
{ {
title: 'caseManagement.featureCase.tableColumnVersion', title: 'caseManagement.featureCase.tableColumnVersion',
slotName: 'version', dataIndex: 'versionName',
dataIndex: 'version',
showInTable: true, showInTable: true,
showTooltip: true, showTooltip: true,
width: 300, width: 300,
@ -137,8 +168,7 @@ const appStore = useAppStore();
}, },
{ {
title: 'caseManagement.featureCase.changeType', title: 'caseManagement.featureCase.changeType',
slotName: 'type', dataIndex: 'relateCaseTypeName',
dataIndex: 'type',
showInTable: true, showInTable: true,
showTooltip: true, showTooltip: true,
width: 300, width: 300,
@ -194,8 +224,10 @@ const appStore = useAppStore();
async function cancelLink(record: any) { async function cancelLink(record: any) {
try { try {
const { id } = record; const { relateId } = record;
await cancelAssociation(id); await cancelAssociation(relateId);
await getFetch();
Message.success(t('common.unLinkSuccess'));
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
@ -211,7 +243,8 @@ const appStore = useAppStore();
try { try {
confirmLoading.value = true; confirmLoading.value = true;
await batchAssociation(params); await batchAssociation(params);
Message.success(t('caseManagement.featureCase.AssociatedSuccess')); await getFetch();
Message.success(t('common.linkSuccess'));
innerVisible.value = false; innerVisible.value = false;
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -59,6 +59,10 @@ export default {
scenarioCase: 'Scenario Case', scenarioCase: 'Scenario Case',
uiCase: 'UI Case', uiCase: 'UI Case',
performanceCase: 'Performance Case', performanceCase: 'Performance Case',
isPlanRelateCase: 'Relate plan',
isPlanRelateCaseTip1: '在测试计划内执行用例时新建或关联的缺陷, 只可在测试计划内取消关联关系;',
isPlanRelateCaseTip2: '缺陷在测试计划内执行用例时关联/新建, 显示为“是”;',
isPlanRelateCaseTip3: '缺陷在缺陷页/用例详情页添加直接关联关系, 未关联上任何测试计划, 显示为“否”;',
searchCase: 'Search by name', searchCase: 'Search by name',
creator: 'Creator', creator: 'Creator',
createTime: 'Create Time', createTime: 'Create Time',

View File

@ -59,6 +59,10 @@ export default {
scenarioCase: '场景用例', scenarioCase: '场景用例',
uiCase: 'UI用例', uiCase: 'UI用例',
performanceCase: '性能用例', performanceCase: '性能用例',
isPlanRelateCase: '关联测试计划',
isPlanRelateCaseTip1: '在测试计划内执行用例时新建或关联的缺陷, 只可在测试计划内取消关联关系;',
isPlanRelateCaseTip2: '缺陷在测试计划内执行用例时关联/新建, 显示为“是”;',
isPlanRelateCaseTip3: '缺陷在缺陷页/用例详情页添加直接关联关系, 未关联上任何测试计划, 显示为“否”;',
searchCase: '通过名称搜索', searchCase: '通过名称搜索',
creator: '创建人', creator: '创建人',
createTime: '创建时间', createTime: '创建时间',