feat(测试跟踪): 用例列表支持批量添加关联需求

--story=1006992 --user=李玉号
【测试跟踪】用例列表支持批量添加关联需求,搜索支持关联需求搜索出对应的用例(1.20分支同步上)
https://www.tapd.cn/55049933/s/1191872
This commit is contained in:
shiziyuan9527 2022-06-29 17:34:45 +08:00 committed by shiziyuan9527
parent aed22e3c4b
commit da3e7b6891
13 changed files with 398 additions and 16 deletions

View File

@ -78,6 +78,17 @@
<property name="object" value="${condition}.reviewStatus"/>
</include>
</if>
<if test="${condition}.demand != null">
<if test="${condition}.demand.operator == 'third_platform'">
and test_case.demand_id in
<foreach collection="${condition}.demand.value" item="v" separator="," open="(" close=")">
#{v}
</foreach>
</if>
<if test="${condition}.demand.operator == 'other_platform'">
and test_case.demand_name like CONCAT('%', #{${condition}.demand.value},'%')
</if>
</if>
<if test="${condition}.customs != null and ${condition}.customs.size() > 0">
<foreach collection="${condition}.customs" item="custom" separator="" open="" close="">
and test_case.id in (

View File

@ -314,6 +314,12 @@ public class TestCaseController {
testCaseService.editTestCaseBath(request);
}
@PostMapping("/batch/relate/demand")
@MsAuditLog(module = OperLogModule.TRACK_TEST_CASE, type = OperLogConstants.BATCH_UPDATE, beforeEvent = "#msClass.getLogDetails(#request.ids)", content = "#msClass.getLogDetails(#request.ids)", msClass = TestCaseService.class)
public void batchRelateDemand(@RequestBody TestCaseBatchRequest request) {
testCaseService.batchRelateDemand(request);
}
@PostMapping("/batch/copy")
@RequiresPermissions(PermissionConstants.PROJECT_TRACK_CASE_READ_COPY)
@MsAuditLog(module = OperLogModule.TRACK_TEST_CASE, type = OperLogConstants.BATCH_ADD, beforeEvent = "#msClass.getLogDetails(#request.ids)", content = "#msClass.getLogDetails(#request.ids)", msClass = TestCaseService.class)

View File

@ -2801,4 +2801,35 @@ public class TestCaseService {
relationshipEdgeService.saveBatch(request);
}
}
public void batchRelateDemand(TestCaseBatchRequest request) {
ServiceUtils.getSelectAllIds(request, request.getCondition(),
(query) -> extTestCaseMapper.selectIds(query));
if (CollectionUtils.isEmpty(request.getIds())) {
return;
}
String demandId = request.getDemandId();
String demandName = request.getDemandName();
if (StringUtils.isBlank(demandId) || (StringUtils.equals(demandId, "other") && StringUtils.isBlank(demandName))) {
return;
}
if (!StringUtils.equals(demandId, "other")) {
demandName = "";
}
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
TestCaseMapper mapper = sqlSession.getMapper(TestCaseMapper.class);
TestCaseExample example = new TestCaseExample();
example.createCriteria().andIdIn(request.getIds());
List<TestCase> testCaseList = testCaseMapper.selectByExample(example);
for (TestCase tc : testCaseList) {
tc.setDemandId(demandId);
tc.setDemandName(demandName);
mapper.updateByPrimaryKey(tc);
}
sqlSession.flushStatements();
if (sqlSession != null && sqlSessionFactory != null) {
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
}
}

View File

@ -30,7 +30,7 @@
<script>
import components from "./search-components";
import {cloneDeep, slice, concat} from "lodash";
import {cloneDeep, concat, slice} from "lodash";
import {_findByKey, _findIndexByKey} from "@/business/components/common/components/search/custom-component";
export default {
@ -144,6 +144,9 @@ export default {
if (component.value !== undefined) {
component.value = source[index].value;
}
if (component.reset && component.reset instanceof Function) {
component.reset();
}
})
this.condition.combine = undefined;
this.$emit('update:condition', this.condition);
@ -174,7 +177,7 @@ export default {
addFilter() {
const index = _findIndexByKey(this.optional.components, this.nullFilterKey);
if (index > -1) {
this.$warning(this.$t('commons.adv_search.add_filter_link'));
this.$warning(this.$t('commons.adv_search.add_filter_link_tip'));
return;
}
let data = {

View File

@ -0,0 +1,77 @@
<template>
<ms-table-search-component v-model="component.operator.value" :component="component" v-bind="$attrs" v-on="$listeners">
<template v-slot="scope">
<el-select v-if="!component.showInput" v-model="scope.component.value" :placeholder="$t('commons.please_select')" size="small"
filterable v-bind="scope.component.props" class="search-select" v-loading="result.loading">
<el-option v-for="op in options" :key="op.value" :label="label(op)" :value="op.value">
<span class="demand-span">{{label(op)}}</span>
</el-option>
</el-select>
<el-input v-model="scope.component.value" v-if="component.showInput" :placeholder="$t('commons.input_content')" size="small"/>
</template>
</ms-table-search-component>
</template>
<script>
import MsTableSearchComponent from "./MsTableSearchComponet";
export default {
name: "MsTableSearchMix",
components: {MsTableSearchComponent},
props: ['component'],
inheritAttrs: false,
data() {
return {
result: {
loading: false
},
options: !(this.component.options instanceof Array) ? [] : this.component.options || []
}
},
created() {
if (!(this.component.options instanceof Array) && this.component.options.url) {
this.result = this.$get(this.component.options.url, response => {
if (response.data) {
response.data.forEach(item => {
this.options.push({
label: item[this.component.options.labelKey],
value: item[this.component.options.valueKey]
})
})
}
})
}
},
computed: {
label() {
return op => {
if (this.component.options.showLabel) {
return this.component.options.showLabel(op);
}
if (op.label) {
return op.label.indexOf(".") !== -1 ? this.$t(op.label) : op.label;
} else {
//
return op.text;
}
}
}
}
}
</script>
<style scoped>
.search-select {
display: inline-block;
width: 100%;
}
.demand-span {
display: inline-block;
max-width: 400px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
margin-right: 5px;
}
</style>

View File

@ -2,29 +2,34 @@
<ms-table-search-component v-model="component.operator.value" :component="component" v-bind="$attrs" v-on="$listeners">
<template v-slot="scope">
<el-select v-model="scope.component.value" :placeholder="$t('commons.please_select')" size="small"
filterable v-bind="scope.component.props" class="search-select">
<el-option v-for="op in options" :key="op.value" :label="label(op)" :value="op.value"></el-option>
filterable v-bind="scope.component.props" class="search-select" v-loading="result.loading">
<el-option v-for="op in options" :key="op.value" :label="label(op)" :value="op.value">
<span class="demand-span">{{label(op)}}</span>
</el-option>
</el-select>
</template>
</ms-table-search-component>
</template>
<script>
import MsTableSearchComponent from "./MsTableSearchComponet";
import MsTableSearchComponent from "./MsTableSearchComponet";
export default {
export default {
name: "MsTableSearchSelect",
components: {MsTableSearchComponent},
props: ['component'],
inheritAttrs: false,
data() {
return {
result: {
loading: false
},
options: !(this.component.options instanceof Array) ? [] : this.component.options || []
}
},
created() {
if (!(this.component.options instanceof Array) && this.component.options.url) {
this.$get(this.component.options.url, response => {
this.result = this.$get(this.component.options.url, response => {
if (response.data) {
response.data.forEach(item => {
this.options.push({
@ -59,4 +64,13 @@
display: inline-block;
width: 100%;
}
.demand-span {
display: inline-block;
max-width: 400px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
margin-right: 5px;
}
</style>

View File

@ -3,13 +3,16 @@ import MsTableSearchDateTimePicker from "./MsTableSearchDateTimePicker";
import MsTableSearchDatePicker from "./MsTableSearchDatePicker";
import MsTableSearchSelect from "./MsTableSearchSelect";
import MsTableSearchInputNumber from "@/business/components/common/components/search/MsTableSearchInputNumber";
import {getCurrentProjectID} from "@/common/js/utils";
import MsTableSearchMix from "@/business/components/common/components/search/MsTableSearchMix";
export default {
MsTableSearchInput,
MsTableSearchDatePicker,
MsTableSearchDateTimePicker,
MsTableSearchSelect,
MsTableSearchInputNumber
MsTableSearchInputNumber,
MsTableSearchMix
}
export const OPERATORS = {
@ -617,6 +620,42 @@ export const END_TIME = {
},
}
// 用例需求
export const CASE_DEMAND = {
key: "demand",
name: "MsTableSearchMix",
label: "test_track.related_requirements",
operator: {
options: [{
label: "test_track.demand.third_platform_demand",
value: "third_platform"
},
{
label: "test_track.demand.other_demand",
value: "other_platform"
}],
change: function (component, value) { // 运算符change事件
component.showInput = value === 'other_platform';
component.value = "";
}
},
reset() { // 重置搜索时执行
this.showInput = false;
},
options: {
url: "/issues/demand/list/" + getCurrentProjectID(),
labelKey: "name",
valueKey: "id",
showLabel: option => {
return option.label;
}
},
props: {
multiple: true,
'collapse-tags': true
}
}
export const TEST_CONFIGS = [NAME, UPDATE_TIME, CREATE_TIME, STATUS, CREATOR, FOLLOW_PEOPLE];
export const PROJECT_CONFIGS = [NAME, UPDATE_TIME, CREATE_TIME, CREATOR];
@ -628,7 +667,7 @@ export const REPORT_CASE_CONFIGS = [NAME, CREATE_TIME, STATUS, CREATOR, TRIGGER_
export const UI_REPORT_CONFIGS = [NAME, TEST_NAME, CREATE_TIME, UI_REPORT_STATUS, CREATOR, TRIGGER_MODE];
// 测试跟踪-测试用例 列表
export const TEST_CASE_CONFIGS = [NAME, TAGS, MODULE, CREATE_TIME, UPDATE_TIME, CREATOR, CASE_REVIEW_STATUS, FOLLOW_PEOPLE];
export const TEST_CASE_CONFIGS = [NAME, TAGS, MODULE, CREATE_TIME, UPDATE_TIME, CREATOR, CASE_REVIEW_STATUS, FOLLOW_PEOPLE, CASE_DEMAND];
export const TEST_PLAN_CONFIGS = [NAME, UPDATE_TIME, CREATE_TIME, PRINCIPAL, TEST_PLAN_STATUS, STAGE, TAGS, FOLLOW_PEOPLE, ACTUAL_START_TIME, ACTUAL_END_TIME, PLAN_START_TIME, PLAN_END_TIME];

View File

@ -37,8 +37,8 @@
<el-form-item v-else-if="form.type === 'tags'" prop="tags" :label="$t('test_track.case.updated_attr_value')">
<ms-input-tag :currentScenario="form" v-if="showInputTag" ref="tag" class="ms-case-input"></ms-input-tag>
<el-checkbox v-model="form.appendTag">
追加标签
<el-tooltip class="item" effect="dark" content="勾选:新增标签;不勾选:覆盖原有标签;" placement="top">
{{ $t('commons.append_tag') }}
<el-tooltip class="item" effect="dark" :content="$t('commons.append_tag_tip')" placement="top">
<i class="el-icon-info"></i>
</el-tooltip>
</el-checkbox>

View File

@ -0,0 +1,152 @@
<template>
<div>
<el-dialog
:title="$t('test_track.please_related_requirements')"
:visible.sync="dialogVisible"
width="30%"
class="batch-edit-dialog"
:destroy-on-close="true"
@close="handleClose"
v-loading="result.loading"
>
<el-form :model="form" label-position="right" size="medium" ref="form">
<el-form-item :label="$t('test_track.related_requirements')" prop="demandId">
<el-cascader v-model="demandValue" :show-all-levels="false" :options="demandOptions"
clearable filterable :filter-method="filterDemand" style="width: 100%;">
<template slot-scope="{ node, data }">
<span class="demand-span" :title="data.label">{{ data.label }}</span>
</template>
</el-cascader>
</el-form-item>
<el-form-item :label="$t('test_track.case.demand_name_id')" prop="demandName" v-if="form.demandId === 'other'">
<el-input v-model="form.demandName"></el-input>
</el-form-item>
</el-form>
<template v-slot:footer>
<ms-dialog-footer @cancel="dialogVisible = false" @confirm="submit()"/>
</template>
</el-dialog>
</div>
</template>
<script>
import {getCurrentProjectID, removeGoBackListener} from "@/common/js/utils";
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
export default {
name: "RelateDemand",
components: {
MsDialogFooter,
},
data() {
return {
result: {},
form: {
demandId: ''
},
demandValue: [],
demandOptions: [],
dialogVisible: false
}
},
watch: {
demandValue() {
if (this.demandValue.length > 0) {
this.form.demandId = this.demandValue[this.demandValue.length - 1];
} else {
this.form.demandId = null;
}
}
},
methods: {
open() {
this.form = {};
this.dialogVisible = true;
this.getDemandOptions();
},
handleClose() {
this.form = {};
this.dialogVisible = false;
removeGoBackListener(this.handleClose);
},
getDemandOptions() {
if (this.demandOptions.length === 0) {
this.result = {loading: true};
this.$get("/issues/demand/list/" + getCurrentProjectID()).then(response => {
this.demandOptions = [];
if (response.data.data && response.data.data.length > 0) {
this.buildDemandCascaderOptions(response.data.data, this.demandOptions, []);
}
this.demandOptions.unshift({
value: 'other',
label: 'Other: ' + this.$t('test_track.case.other'),
platform: 'Other'
});
if (this.form.demandId === 'other') {
this.demandValue = ['other'];
}
this.result = {loading: false};
}).catch(() => {
this.demandOptions.unshift({
value: 'other',
label: 'Other: ' + this.$t('test_track.case.other'),
platform: 'Other'
});
if (this.form.demandId === 'other') {
this.demandValue = ['other'];
}
this.result = {loading: false};
});
}
},
buildDemandCascaderOptions(data, options, pathArray) {
data.forEach(item => {
let option = {
label: item.platform + ': ' + item.name,
value: item.id
}
options.push(option);
pathArray.push(item.id);
if (item.id === this.form.demandId) {
this.demandValue = [...pathArray]; //
}
if (item.children && item.children.length > 0) {
option.children = [];
this.buildDemandCascaderOptions(item.children, option.children, pathArray);
}
pathArray.pop();
});
},
filterDemand(node, keyword) {
return !!(keyword && node.text.toLowerCase().indexOf(keyword.toLowerCase()) !== -1);
},
submit() {
if (!this.form.demandId) {
this.$warning(this.$t('test_track.demand.relate_is_null_warn'));
return;
}
if (this.form.demandId === 'other' && !this.form.demandName) {
this.$warning(this.$t('test_track.demand.relate_name_is_null_warn'));
return;
}
this.$emit('batchRelate', this.form);
this.form = {demandId: ''};
this.demandValue = [];
this.dialogVisible = false;
}
}
}
</script>
<style scoped>
.demand-span {
display: inline-block;
max-width: 400px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
margin-right: 5px;
}
</style>

View File

@ -216,6 +216,8 @@
<batch-move @refresh="refresh" @moveSave="moveSave" ref="testBatchMove" :public-enable="publicEnable"
@copyPublic="copyPublic"/>
<relate-demand ref="relateDemand" @batchRelate="_batchRelateDemand"/>
<test-case-preview ref="testCasePreview" :loading="rowCaseResult.loading"/>
<relationship-graph-drawer v-xpack :graph-data="graphData" ref="relationshipGraph"/>
@ -252,12 +254,14 @@ import {
buildBatchParam,
deepClone,
getCustomFieldBatchEditOption,
getCustomFieldValue, getCustomTableHeader,
getCustomFieldValue,
getCustomTableHeader,
getCustomTableWidth,
getLastTableSortField,
getPageInfo,
getTableHeaderWithCustomFields,
initCondition, parseCustomFilesForList,
initCondition,
parseCustomFilesForList,
} from "@/common/js/tableUtils";
import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate";
import PlanStatusTableItem from "@/business/components/track/common/tableItems/plan/PlanStatusTableItem";
@ -282,6 +286,7 @@ import MsTableAdvSearchBar from "@/business/components/common/components/search/
import ListItemDeleteConfirm from "@/business/components/common/components/ListItemDeleteConfirm";
import {getAdvSearchCustomField} from "@/business/components/common/components/search/custom-component";
import MsSearch from "@/business/components/common/components/search/MsSearch";
import RelateDemand from "@/business/components/track/case/components/RelateDemand";
const requireComponent = require.context('@/business/components/xpack/', true, /\.vue$/);
const relationshipGraphDrawer = requireComponent.keys().length > 0 ? requireComponent("./graph/RelationshipGraphDrawer.vue") : {};
@ -289,6 +294,7 @@ const relationshipGraphDrawer = requireComponent.keys().length > 0 ? requireComp
export default {
name: "TestCaseList",
components: {
RelateDemand,
MsSearch,
ListItemDeleteConfirm,
MsTableAdvSearchBar,
@ -382,6 +388,10 @@ export default {
handleClick: this.handleDeleteBatchToGc,
permissions: ['PROJECT_TRACK_CASE:READ+BATCH_DELETE']
},
{
name: this.$t('test_track.demand.batch_relate'),
handleClick: this.openRelateDemand
},
{
name: this.$t('test_track.case.generate_dependencies'),
isXPack: true,
@ -1118,6 +1128,24 @@ export default {
});
},
openRelateDemand() {
this.$refs.relateDemand.open();
},
_batchRelateDemand(form) {
if (form.demandId !== 'other') {
form.demandName = '';
}
let ids = this.$refs.table.selectIds;
let param = {};
param.ids = ids;
param.condition = this.condition;
param.demandId = form.demandId;
param.demandName = form.demandName;
this.$post('/test/case/batch/relate/demand', param, () => {
this.$success(this.$t('commons.save_success'));
this.refresh();
});
},
handleDeleteBatchToPublic() {
this.$alert(this.$t('test_track.case.delete_confirm') + "", '', {
confirmButtonText: this.$t('commons.confirm'),

View File

@ -343,6 +343,8 @@ export default {
},
image: 'Image',
tag: 'Tag',
append_tag: 'Append Tag',
append_tag_tip: 'Checked: add a new label; unchecked: overwrite the original label;',
module: {
select_module: "Select module",
default_module: "Default module",
@ -2036,7 +2038,12 @@ export default {
other_test_name: 'Other Test Name',
demand: {
id: 'Demand ID',
name: 'Demand Name'
name: 'Demand Name',
batch_relate: 'Bulk Association Requirements',
relate_is_null_warn: 'The associated requirement cannot be empty!',
relate_name_is_null_warn: 'Requirement name cannot be empty!',
third_platform_demand: "Third platform demand",
other_demand: "Other demand"
},
step_model: 'Step Model',
automatic_status_update: "Automatic Status Update",

View File

@ -344,6 +344,8 @@ export default {
},
image: '镜像',
tag: '标签',
append_tag: '追加标签',
append_tag_tip: '勾选:新增标签;不勾选:覆盖原有标签;',
module: {
select_module: "选择模块",
default_module: "默认模块",
@ -2250,7 +2252,12 @@ export default {
},
demand: {
id: '需求ID',
name: '需求名称'
name: '需求名称',
batch_relate: '批量关联需求',
relate_is_null_warn: '关联需求不能为空!',
relate_name_is_null_warn: '需求名称不能为空!',
third_platform_demand: "三方平台需求",
other_demand: "其他需求"
},
step_model: '步骤模型',
review: {

View File

@ -344,6 +344,8 @@ export default {
},
image: '鏡像',
tag: '標簽',
append_tag: '追加標籤',
append_tag_tip: '勾選:新增標籤;不勾選:覆蓋原有標籤;',
module: {
select_module: "選擇模塊",
default_module: "默認模塊",
@ -2244,7 +2246,12 @@ export default {
},
demand: {
id: '需求ID',
name: '需求名稱'
name: '需求名稱',
batch_relate: '批量關聯需求',
relate_is_null_warn: '關聯需求不能為空!',
relate_name_is_null_warn: '需求名稱不能為空!',
third_platform_demand: "三方平台需求",
other_demand: "其他需求"
},
step_model: '步驟模型',
review: {