fix(用例管理): 修复功能用例详情相关权限以及评审评论详情增加用例等级问题

--bug=1037142 --user=郭雨琦 https://www.tapd.cn/55049933/bugtrace/bugs/view/1155049933001037142
--bug=1036754 --user=郭雨琦 https://www.tapd.cn/55049933/bugtrace/bugs/view/1155049933001036754
--bug=1036044 --user=郭雨琦 https://www.tapd.cn/55049933/bugtrace/bugs/view/1155049933001036044
This commit is contained in:
guoyuqi 2024-03-19 19:43:22 +08:00 committed by Craftsman
parent a3aad0b82c
commit 4adc3c3355
15 changed files with 207 additions and 41 deletions

View File

@ -51,7 +51,7 @@ public class FunctionalCaseCommentController {
@GetMapping("/get/list/{caseId}")
@Operation(summary = "用例管理-功能用例-用例评论-获取用例评论")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_COMMENT)
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ)
@CheckOwner(resourceId = "#caseId", resourceType = "functional_case")
public List<FunctionalCaseCommentDTO> getCommentList(@PathVariable String caseId) {
return functionalCaseCommentService.getCommentList(caseId);

View File

@ -61,4 +61,7 @@ public class ReviewFunctionalCaseDTO implements Serializable {
@Schema(description = "只看我的评审状态")
private String myStatus;
@Schema(description = "自定义字段集合")
private List<FunctionalCaseCustomFieldDTO> customFields;
}

View File

@ -111,6 +111,12 @@
and functional_case.version_id in
<include refid="io.metersphere.system.mapper.BaseMapper.filterInWrapper"/>
</when>
<when test="key=='caseLevel'">
and functional_case.id in (
select case_id from functional_case_custom_field where `value` in
<include refid="io.metersphere.system.mapper.BaseMapper.filterInWrapper"/>
)
</when>
<when test="key.startsWith('custom_single')">
and functional_case.id in (
select resource_id from custom_field_test_case where concat('custom_single-',field_id) =

View File

@ -26,12 +26,16 @@ import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.domain.CustomFieldOption;
import io.metersphere.system.domain.UserRoleRelation;
import io.metersphere.system.domain.UserRoleRelationExample;
import io.metersphere.system.dto.sdk.BaseTreeNode;
import io.metersphere.system.dto.sdk.OptionDTO;
import io.metersphere.system.mapper.UserRoleRelationMapper;
import io.metersphere.system.notice.constants.NoticeConstants;
import io.metersphere.system.service.BaseCustomFieldOptionService;
import io.metersphere.system.service.BaseCustomFieldService;
import io.metersphere.system.service.UserLoginService;
import io.metersphere.system.uid.IDGenerator;
import io.metersphere.system.utils.ServiceUtils;
import jakarta.annotation.Resource;
@ -101,6 +105,14 @@ public class CaseReviewFunctionalCaseService {
private PermissionCheckService permissionCheckService;
@Resource
private CaseReviewMapper caseReviewMapper;
@Resource
private FunctionalCaseCustomFieldService functionalCaseCustomFieldService;
@Resource
private BaseCustomFieldService baseCustomFieldService;
@Resource
private BaseCustomFieldOptionService baseCustomFieldOptionService;
@Resource
private UserLoginService userLoginService;
private static final String CASE_MODULE_COUNT_ALL = "all";
@ -150,12 +162,14 @@ public class CaseReviewFunctionalCaseService {
caseStatusMap = new LinkedHashMap<>();
}
Map<String, List<FunctionalCaseCustomFieldDTO>> collect = getCaseCustomFiledMap(caseIds);
list.forEach(item -> {
item.setModuleName(moduleMap.get(item.getModuleId()));
item.setVersionName(versionMap.get(item.getVersionId()));
item.setReviewers(Collections.singletonList(userIdMap.get(item.getCaseId())));
item.setReviewNames(Collections.singletonList(userNameMap.get(item.getCaseId())));
item.setCustomFields(collect.get(item.getCaseId()));
if (request.isViewStatusFlag()) {
List<CaseReviewHistory> histories = caseStatusMap.get(item.getCaseId());
if (CollectionUtils.isNotEmpty(histories)) {
@ -170,6 +184,23 @@ public class CaseReviewFunctionalCaseService {
return list;
}
public Map<String, List<FunctionalCaseCustomFieldDTO>> getCaseCustomFiledMap(List<String> ids) {
List<FunctionalCaseCustomFieldDTO> customFields = functionalCaseCustomFieldService.getCustomFieldsByCaseIds(ids);
customFields.forEach(customField -> {
if (customField.getInternal()) {
customField.setFieldName(baseCustomFieldService.translateInternalField(customField.getFieldName()));
}
});
List<String> fieldIds = customFields.stream().map(FunctionalCaseCustomFieldDTO::getFieldId).toList();
List<CustomFieldOption> fieldOptions = baseCustomFieldOptionService.getByFieldIds(fieldIds);
Map<String, List<CustomFieldOption>> customOptions = fieldOptions.stream().collect(Collectors.groupingBy(CustomFieldOption::getFieldId));
customFields.forEach(customField -> {
customField.setOptions(customOptions.get(customField.getFieldId()));
});
return customFields.stream().collect(Collectors.groupingBy(FunctionalCaseCustomFieldDTO::getCaseId));
}
private String getMyStatus(List<CaseReviewHistory> histories, String viewStatusUserId) {
List<CaseReviewHistory> list = histories.stream().filter(history -> StringUtils.equalsIgnoreCase(history.getCreateUser(), viewStatusUserId)).toList();
if (CollectionUtils.isNotEmpty(list)) {

View File

@ -23,6 +23,7 @@
<span class="text-[var(--color-text-4)]">({{ element.childComments?.length }})</span>
</div>
<div
v-if="hasAnyPermission(['PROJECT_BUG:READ+COMMENT', 'FUNCTIONAL_CASE:READ+COMMENT'])"
class="comment-btn hover:bg-[var(--color-bg-3)]"
:class="{ 'bg-[var(--color-text-n8)]': status === 'reply' }"
@click="replyClick"
@ -63,6 +64,7 @@
import { useI18n } from '@/hooks/useI18n';
import useUserStore from '@/store/modules/user/index';
import { hasAnyPermission } from '@/utils/permission';
import { CommentItem } from './types';
@ -81,7 +83,10 @@
//
const hasAuth = computed(() => {
return props.element.createUser === userStore.id;
return (
props.element.createUser === userStore.id &&
hasAnyPermission(['PROJECT_BUG:READ+COMMENT', 'FUNCTIONAL_CASE:READ+COMMENT'])
);
});
const status = defineModel<'normal' | 'edit' | 'reply' | 'delete'>('status', { default: 'normal' });

View File

@ -318,7 +318,7 @@
.ms-drawer-no-mask {
left: auto;
.arco-drawer {
box-shadow: -1px 0 4px 0 rgb(2 2 2 / 10%);
box-shadow: 0 4px 10px -1px rgb(100 100 102 / 15%);
}
}
.ms-drawer-no-content-padding {

View File

@ -952,7 +952,7 @@
versionId: '',
refId: '',
});
Message.success(t('common.updateSuccess'));
Message.success(t('case.detail.execute.success'));
cancelBatchExecute();
loadCaseListAndResetSelector();
} catch (error) {

View File

@ -519,6 +519,9 @@
// );
const total = ref<number>(0);
async function initBugList() {
if (!hasAnyPermission(['PROJECT_BUG:READ'])) {
return;
}
const res = await getBugList({
current: 1,
pageSize: 10,

View File

@ -32,10 +32,13 @@
</template>
<template v-if="(keyword || '').trim() === ''" #empty>
<div class="flex w-full items-center justify-center text-[var(--color-text-4)]">
{{ t('caseManagement.caseReview.tableNoData') }}
<span v-if="hasAnyPermission(['FUNCTIONAL_CASE:READ+UPDATE'])">{{
t('caseManagement.caseReview.tableNoData')
}}</span>
<span v-else>{{ t('caseManagement.featureCase.tableNoData') }}</span>
<a-dropdown @select="handleSelect">
<MsButton class="ml-[8px]">
<MsButton v-permission="['FUNCTIONAL_CASE:READ+UPDATE']" class="ml-[8px]">
{{ t('caseManagement.featureCase.linkCase') }}
</MsButton>
<template #content>
@ -89,6 +92,7 @@
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { hasAnyPermission } from '@/utils/permission';
import type { TableQueryParams } from '@/models/common';
import { TableKeyEnum } from '@/enums/tableEnum';

View File

@ -132,25 +132,21 @@
}
async function getAllCommentList() {
if (hasAnyPermission(['FUNCTIONAL_CASE:READ+COMMENT'])) {
switch (activeComment.value) {
case 'caseComment':
await initCommentList();
featureCaseStore.getCaseCounts(props.caseId);
break;
case 'reviewComment':
await initReviewCommentList();
featureCaseStore.getCaseCounts(props.caseId);
break;
case 'executiveComment':
await initCommentList();
featureCaseStore.getCaseCounts(props.caseId);
break;
default:
break;
}
} else {
Message.error(t('common.noPermission'));
switch (activeComment.value) {
case 'caseComment':
await initCommentList();
featureCaseStore.getCaseCounts(props.caseId);
break;
case 'reviewComment':
await initReviewCommentList();
featureCaseStore.getCaseCounts(props.caseId);
break;
case 'executiveComment':
await initCommentList();
featureCaseStore.getCaseCounts(props.caseId);
break;
default:
break;
}
}

View File

@ -6,21 +6,53 @@
><span class="one-text-line text-[rgb(var(--primary-5))]">({{ (record.children || []).length || 0 }})</span>
</template>
<template #operation="{ record }">
<MsButton @click="emit('cancel', record)">
<MsButton
v-permission="['FUNCTIONAL_CASE:READ+ADD', 'FUNCTIONAL_CASE:READ+UPDATE', 'FUNCTIONAL_CASE:READ+DELETE']"
@click="emit('cancel', record)"
>
{{ t('caseManagement.featureCase.cancelAssociation') }}
</MsButton>
<MsButton v-if="record.demandPlatform === pageConfig.platformName" @click="emit('update', record)">
<MsButton
v-if="record.demandPlatform === pageConfig.platformName"
v-permission="['FUNCTIONAL_CASE:READ+ADD', 'FUNCTIONAL_CASE:READ+UPDATE', 'FUNCTIONAL_CASE:READ+DELETE']"
@click="emit('update', record)"
>
{{ t('common.edit') }}
</MsButton>
</template>
<template v-if="(props.funParams.keyword || '').trim() === '' && props.showEmpty" #empty>
<div class="flex w-full items-center justify-center text-[var(--color-text-4)]">
{{ t('caseManagement.caseReview.tableNoData') }}
<MsButton class="ml-[8px]" @click="emit('associate')">
<span
v-if="
hasAnyPermission(['FUNCTIONAL_CASE:READ+ADD', 'FUNCTIONAL_CASE:READ+UPDATE', 'FUNCTIONAL_CASE:READ+DELETE'])
"
>
{{ t('caseManagement.caseReview.tableNoData') }}
</span>
<span v-else> {{ t('caseManagement.featureCase.tableNoData') }} </span>
<MsButton
v-if="
hasAnyPermission(['FUNCTIONAL_CASE:READ+ADD', 'FUNCTIONAL_CASE:READ+UPDATE', 'FUNCTIONAL_CASE:READ+DELETE'])
"
class="ml-[8px]"
@click="emit('associate')"
>
{{ t('caseManagement.featureCase.associatedDemand') }}
</MsButton>
{{ t('caseManagement.featureCase.or') }}
<MsButton class="ml-[8px]" @click="emit('create')">
<span
v-if="
hasAnyPermission(['FUNCTIONAL_CASE:READ+ADD', 'FUNCTIONAL_CASE:READ+UPDATE', 'FUNCTIONAL_CASE:READ+DELETE'])
"
>
{{ t('caseManagement.featureCase.or') }}
</span>
<MsButton
v-if="
hasAnyPermission(['FUNCTIONAL_CASE:READ+ADD', 'FUNCTIONAL_CASE:READ+UPDATE', 'FUNCTIONAL_CASE:READ+DELETE'])
"
class="ml-[8px]"
@click="emit('create')"
>
{{ t('caseManagement.featureCase.addDemand') }}
</MsButton>
</div>
@ -41,6 +73,7 @@
import { useAppStore } from '@/store';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { characterLimit } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import type { DemandItem } from '@/models/caseManagement/featureCase';

View File

@ -288,7 +288,9 @@
}
function openDemandUrl(record: DemandItem) {
window.open(record.demandUrl);
if (record.demandUrl) {
window.open(record.demandUrl);
}
}
const platformInfo = ref<Record<string, any>>({});

View File

@ -51,8 +51,11 @@
</template>
<template v-if="(keyword || '').trim() === ''" #empty>
<div class="flex w-full items-center justify-center text-[var(--color-text-4)]">
{{ t('caseManagement.caseReview.tableNoData') }}
<MsButton class="ml-[8px]" @click="addCase">
<span v-if="hasAnyPermission(['FUNCTIONAL_CASE:READ+UPDATE'])">{{
t('caseManagement.caseReview.tableNoData')
}}</span>
<span v-else>{{ t('caseManagement.featureCase.tableNoData') }}</span>
<MsButton v-if="hasAnyPermission(['FUNCTIONAL_CASE:READ+UPDATE'])" class="ml-[8px]" @click="addCase">
{{
showType === 'preposition'
? t('caseManagement.featureCase.addPresetCase')
@ -88,6 +91,7 @@
import { useAppStore } from '@/store';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { characterLimit } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
const featureCaseStore = useFeatureCaseStore();
// const activeTab = computed(() => featureCaseStore.activeTab);

View File

@ -32,10 +32,36 @@
v-on="propsEvent"
@batch-action="handleTableBatch"
>
<template #resultTitle>
<div class="flex items-center text-[var(--color-text-3)]">
{{ t('caseManagement.caseReview.reviewResult') }}
</div>
<template #resultTitle="{ columnConfig }">
<a-trigger
v-model:popup-visible="statusFilterVisible"
trigger="click"
@popup-visible-change="handleFilterHidden"
>
<a-button type="text" class="arco-btn-text--secondary p-[8px_4px]" @click="statusFilterVisible = true">
<div class="font-medium">
{{ t(columnConfig.title as string) }}
</div>
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</a-button>
<template #content>
<div class="arco-table-filters-content">
<div class="flex items-center justify-center px-[6px] py-[2px]">
<a-checkbox-group v-model:model-value="statusFilters" direction="vertical" size="small">
<a-checkbox v-for="key of Object.keys(reviewResultMap)" :key="key" :value="key">
<a-tag
:color="reviewResultMap[key].color"
:class="[reviewResultMap[key].class, 'px-[4px]']"
size="small"
>
{{ t(reviewResultMap[key].label) }}
</a-tag>
</a-checkbox>
</a-checkbox-group>
</div>
</div>
</template>
</a-trigger>
</template>
<template #num="{ record }">
<a-tooltip :content="record.num">
@ -44,6 +70,23 @@
</a-button>
</a-tooltip>
</template>
<template #caseLevel="{ record }">
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="record.caseLevel" /></span>
</template>
<template #caseLevelFilter="{ columnConfig }">
<TableFilter
v-model:visible="caseFilterVisible"
v-model:status-filters="caseFilters"
:title="(columnConfig.title as string)"
:list="caseLevelList"
value-key="value"
@search="searchCase()"
>
<template #item="{ item }">
<div class="flex"> <caseLevel :case-level="item.text" /></div>
</template>
</TableFilter>
</template>
<template #reviewNames="{ record }">
<a-tooltip :content="record.reviewNames.join('、')">
<div class="one-line-text">{{ record.reviewNames.join('、') }}</div>
@ -246,7 +289,9 @@
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { MsFileItem } from '@/components/pure/ms-upload/types';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import MsSelect from '@/components/business/ms-select';
import TableFilter from '@/views/case-management/caseManagementFeature/components/tableFilter.vue';
import {
batchChangeReviewer,
@ -256,7 +301,7 @@
getReviewDetailCasePage,
getReviewUsers,
} from '@/api/modules/case-management/caseReview';
import { editorUploadFile } from '@/api/modules/case-management/featureCase';
import { editorUploadFile, getCaseDefaultFields } from '@/api/modules/case-management/featureCase';
import { getProjectMemberCommentOptions } from '@/api/modules/project-management/projectMember';
import { reviewResultMap } from '@/config/caseManagement';
import { useI18n } from '@/hooks/useI18n';
@ -271,6 +316,13 @@
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
const caseLevelFields = ref<Record<string, any>>({});
const caseFilterVisible = ref(false);
const caseFilters = ref<string[]>([]);
const caseLevelList = computed(() => {
return caseLevelFields.value?.options || [];
});
const props = defineProps<{
activeFolder: string | number;
onlyMine: boolean;
@ -293,6 +345,9 @@
const filterConfigList = ref<FilterFormItem[]>([]);
const tableParams = ref<Record<string, any>>({});
const statusFilterVisible = ref(false);
const statusFilters = ref<string[]>(Object.keys(reviewResultMap));
const hasOperationPermission = computed(() =>
hasAnyPermission(['CASE_REVIEW:READ+REVIEW', 'CASE_REVIEW:READ+RELEVANCE'])
);
@ -319,6 +374,15 @@
showTooltip: true,
width: 200,
},
{
title: 'caseManagement.featureCase.tableColumnLevel',
slotName: 'caseLevel',
dataIndex: 'caseLevel',
titleSlotName: 'caseLevelFilter',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'caseManagement.caseReview.reviewer',
dataIndex: 'reviewNames',
@ -393,6 +457,7 @@
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
keyword: keyword.value,
viewFlag: props.onlyMine,
filter: { status: statusFilters.value, caseLevel: caseFilters.value },
combine: filter
? {
...filter.combine,
@ -410,6 +475,12 @@
});
}
function handleFilterHidden(val: boolean) {
if (!val) {
searchCase();
}
}
onBeforeMount(() => {
searchCase();
});
@ -548,6 +619,13 @@
});
}
//
async function getCaseLevelFields() {
const result = await getCaseDefaultFields(appStore.currentProjectId);
caseLevelFields.value = result.customFields.find((item: any) => item.internal && item.fieldName === '用例等级');
caseFilters.value = caseLevelFields.value?.options.map((item: any) => item.text);
}
//
async function reReview() {
try {
@ -715,6 +793,7 @@
}
onBeforeMount(async () => {
getCaseLevelFields();
const [, memberRes] = await Promise.all([
initReviewers(),
getProjectMemberCommentOptions(appStore.currentProjectId, keyword.value),

View File

@ -64,7 +64,7 @@ export default {
'caseManagement.caseReview.belongModulePlaceholder': '请选择该评审所属模块',
'caseManagement.caseReview.reviewerPlaceholder': '请选择评审人',
'caseManagement.caseReview.defaultReviewer': '默认评审人',
'caseManagement.caseReview.defaultReviewerRequired': '默认评审人',
'caseManagement.caseReview.defaultReviewerRequired': '默认评审人不能为空',
'caseManagement.caseReview.defaultReviewerTip': '新添加的用例,评审人为默认评审人',
'caseManagement.caseReview.pickCases': '选择需要评审的用例',
'caseManagement.caseReview.switchProject': '切换项目:',