feat(测试跟踪): 缺陷管理支持EXCEL导入导出

--story=1010310 --user=宋昌昌 【测试跟踪】缺陷管理支持通过excel导入导出 https://www.tapd.cn/55049933/s/1295113
This commit is contained in:
song-cc-rock 2022-11-11 19:14:07 +08:00 committed by 刘瑞斌
parent ae9b90f18a
commit d3eb043730
35 changed files with 2222 additions and 153 deletions

View File

@ -170,6 +170,7 @@ export default {
import_update: "Import Update",
import_tip1: "The ID is required when the \"Test Case Custom ID\" switch is turned on in the project settings",
import_tip2: "ID is required when importing and updating",
import_type_require_tips: "Please select the import type!",
import_file_tips: "Please upload the file first!",
import_refresh_tips: "Content has been updated, please reopen the edit page!",
import_format: "Import Format",
@ -443,10 +444,11 @@ export default {
platform_tip: "Integrated defect management platform in the system setting-workspace-service integration can submit defects to the designated defect management platform",
input_title: "Please enter title",
id: "Issue ID",
title: "Issue Title",
description: "Issue Describe",
title: "Title",
description: "Description",
status: "Issue Status",
platform: "Platform",
issue_platform: "Issue platform",
issue_project: "Project",
operate: "Operate",
close: "Close",
@ -474,8 +476,11 @@ export default {
sync_bugs: "Synchronization Issue",
sync_complete: "Synchronization complete",
issue_sync_tip: "The current project is synchronizing defects, please wait!",
import_bugs: "Import Issue",
export_bugs: "Export Issue",
save_before_open_comment: "Please save issue before comment",
delete_tip: "Confirm Delete Issue",
batch_delete_tip: "Confirm Batch Delete Issue",
check_id_exist: "Check",
save_project_first: "Please save the project first",
tapd_status_new: "New",
@ -486,6 +491,12 @@ export default {
tapd_status_closed: "Closed",
tapd_status_resolved: "Resolved",
please_choose_platform_status: "Please select platform status",
import_type: "Import type",
import_file_limit_tips: "Only XLS/XLSX files can be uploaded, and the size is not larger than 100 MB",
check_select: "Please check issues",
export: "Export issue",
batch_delete_issue: "Batch delete issues",
import_type_tips: "Cover mode :<br>1. If the defect ID already exists, the original defect of the system will be overwritten. <br>2. If the defect ID does not exist or is missing, a new defect is added; <br> Not cover mode :<br>1. If the defect ID already exists, it will not be changed; <br>2. If the defect ID does not exist or is missing, a new defect is added;"
},
report: {
name: "Test Plan Report",

View File

@ -161,6 +161,7 @@ export default {
import_update: "导入更新",
import_tip1: "项目设置中“测试用例自定义ID” 开关开启时ID为必填项",
import_tip2: "导入更新时ID为必填项",
import_type_require_tips: "请选择导入模式!",
import_file_tips: "请先上传文件!",
import_format: "导入格式",
select_import_field: "选择导出字段",
@ -451,6 +452,7 @@ export default {
status: "缺陷状态",
issue_project: "所属项目",
platform: "平台",
issue_platform: "缺陷平台",
operate: "操作",
close: "关闭缺陷",
delete: "删除缺陷",
@ -477,8 +479,11 @@ export default {
sync_bugs: "同步缺陷",
sync_complete: "同步完成",
issue_sync_tip: "当前项目正在同步缺陷, 请稍等!",
import_bugs: "导入缺陷",
export_bugs: "导出缺陷",
save_before_open_comment: "请先保存缺陷再添加评论",
delete_tip: "确认删除缺陷:",
batch_delete_tip: "确认批量删除缺陷",
check_id_exist: "检查",
save_project_first: "请先保存项目",
tapd_status_new: "新",
@ -488,7 +493,13 @@ export default {
tapd_status_verified: "已验证",
tapd_status_closed: "已关闭",
tapd_status_resolved: "已解决",
please_choose_platform_status: "请选择平台状态"
please_choose_platform_status: "请选择平台状态",
import_type: "导入模式",
import_file_limit_tips: "只能上传XLS/XLSX文件, 且不超过100M",
check_select: "请勾选缺陷",
export: "导出缺陷",
batch_delete_issue: "批量删除",
import_type_tips: "覆盖模式:<br>1.缺陷ID已存在,则覆盖系统原缺陷;<br>2.缺陷ID不存在或为空缺失,则新增缺陷;<br>不覆盖模式:<br>1.缺陷ID已存在,则不作变更;<br>2.缺陷ID不存在或为空缺失,则新增缺陷;"
},
report: {
name: "测试计划报告",

View File

@ -161,6 +161,7 @@ export default {
import_update: "導入更新",
import_tip1: "項目設置中“測試用例自定義ID” 開關開啟時ID為必填項",
import_tip2: "導入更新時ID為必填項",
import_type_require_tips: "請選擇導入模式!",
import_file_tips: "請先上傳文件!",
import_format: "導入格式",
select_import_field: "選擇導出字段",
@ -451,6 +452,7 @@ export default {
status: "缺陷狀態",
issue_project: "所屬項目",
platform: "平臺",
issue_platform: "缺陷平臺",
operate: "操作",
close: "關閉缺陷",
delete: "刪除缺陷",
@ -477,8 +479,11 @@ export default {
sync_bugs: "同步缺陷",
sync_complete: "同步完成",
issue_sync_tip: "當前項目正在同步缺陷, 請稍等!",
import_bugs: "導入缺陷",
export_bugs: "導出缺陷",
save_before_open_comment: "請先保存缺陷再添加評論",
delete_tip: "確認刪除缺陷:",
batch_delete_tip: "確認批量刪除缺陷",
check_id_exist: "檢查",
save_project_first: "請先保存項目",
tapd_status_new: "新",
@ -488,7 +493,13 @@ export default {
tapd_status_verified: "已驗證",
tapd_status_closed: "已關閉",
tapd_status_resolved: "已解決",
please_choose_platform_status: "請選擇平臺狀態"
please_choose_platform_status: "請選擇平臺狀態",
import_type: "導入模式",
import_file_limit_tips: "只能上傳XLS/XLSX文件, 且不超過100M",
check_select: "請勾選缺陷",
export: "導出缺陷",
batch_delete_issue: "批量刪除",
import_type_tips: "覆蓋模式:<br>1.缺陷ID已存在,則覆蓋系統原缺陷;<br>2.缺陷ID不存在或爲空缺失,則新增缺陷;<br>不覆蓋模式:<br>1.缺陷ID已存在,則不作變更;<br>2.缺陷ID不存在或爲空缺失,則新增缺陷;"
},
report: {
name: "測試計劃報告",

View File

@ -18,7 +18,8 @@ public enum CustomFieldType {
INT("int", false),
FLOAT("float", false),
MULTIPLE_INPUT("multipleInput", false),
RICH_TEXT("richText", false);
RICH_TEXT("richText", false),
CASCADING_SELECT("cascadingSelect", false);
private String value;
private Boolean hasOption;

View File

@ -8,5 +8,6 @@ import java.util.List;
@Data
public class IssueTemplateDao extends IssueTemplate {
List<CustomFieldDao> customFields;
private List<CustomFieldDao> customFields;
private Boolean isThirdTemplate;
}

View File

@ -35,4 +35,9 @@ public class IssuesDao extends IssuesWithBLOBs {
private String fieldName;
private String fieldType;
private String fieldValue;
/**
* 导出评论
*/
private String comment;
}

View File

@ -59,4 +59,9 @@ public class IssuesRequest extends BaseQueryRequest {
* 自定义字段ID
*/
private String customFieldId;
/**
* 缺陷导出勾选ID
*/
private List<String> exportIds;
}

View File

@ -66,4 +66,8 @@ public class IssuesUpdateRequest extends IssuesWithBLOBs {
* 取消关联文件应用ID
*/
private List<String> unRelateFileMetaIds = new ArrayList<>();
private List<String> batchDeleteIds;
private Boolean batchDeleteAll;
}

View File

@ -15,4 +15,11 @@ public interface ExtIssueCommentMapper {
*/
List<IssueCommentDTO> getComments(@Param("issueId") String issueId);
/**
* 获取多条用例的的评论
* @param issueIds
* @return
*/
List<IssueCommentDTO> getCommentsByIssueIds(@Param("issueIds") List<String> issueIds);
}

View File

@ -12,4 +12,17 @@
order by issue_comment.update_time desc
</select>
<select id="getCommentsByIssueIds" resultType="io.metersphere.dto.IssueCommentDTO"
parameterType="java.lang.String">
select *, user.name as authorName
from issue_comment,
user
where issue_comment.author = user.id
and issue_id in
<foreach collection="issueIds" item="id" separator="," open="(" close=")">
#{id}
</foreach>
order by issue_comment.update_time desc
</select>
</mapper>

View File

@ -229,7 +229,12 @@
#{value}
</foreach>
</if>
<if test="!request.selectAll and request.exportIds != null and request.exportIds.size > 0">
and issues.id in
<foreach collection="request.exportIds" item="value" separator="," open="(" close=")">
#{value}
</foreach>
</if>
<if test="request.filters != null and request.filters.size() > 0">
<foreach collection="request.filters.entrySet()" index="key" item="values">
<if test="values != null and values.size() > 0">

View File

@ -11,25 +11,29 @@ import io.metersphere.commons.constants.OperLogModule;
import io.metersphere.commons.constants.PermissionConstants;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.dto.*;
import io.metersphere.dto.IssuesStatusCountDao;
import io.metersphere.excel.domain.ExcelResponse;
import io.metersphere.log.annotation.MsAuditLog;
import io.metersphere.notice.annotation.SendNotice;
import io.metersphere.xpack.track.dto.*;
import io.metersphere.service.issue.domain.jira.JiraIssueType;
import io.metersphere.service.issue.domain.zentao.ZentaoBuild;
import io.metersphere.request.issues.IssueExportRequest;
import io.metersphere.request.issues.IssueImportRequest;
import io.metersphere.request.issues.JiraIssueTypeRequest;
import io.metersphere.request.issues.PlatformIssueTypeRequest;
import io.metersphere.request.testcase.AuthUserIssueRequest;
import io.metersphere.request.testcase.IssuesCountRequest;
import io.metersphere.service.BaseCheckPermissionService;
import io.metersphere.service.IssuesService;
import io.metersphere.service.issue.domain.jira.JiraIssueType;
import io.metersphere.service.issue.domain.zentao.ZentaoBuild;
import io.metersphere.xpack.track.dto.*;
import io.metersphere.xpack.track.dto.request.IssuesRequest;
import io.metersphere.xpack.track.dto.request.IssuesUpdateRequest;
import io.metersphere.service.IssuesService;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@RequestMapping("issues")
@ -38,6 +42,8 @@ public class IssuesController {
@Resource
private IssuesService issuesService;
@Resource
private BaseCheckPermissionService baseCheckPermissionService;
@PostMapping("/list/{goPage}/{pageSize}")
@RequiresPermissions(PermissionConstants.PROJECT_TRACK_ISSUE_READ)
@ -115,12 +121,20 @@ public class IssuesController {
}
@GetMapping("/delete/{id}")
@RequiresPermissions(PermissionConstants.PROJECT_TRACK_ISSUE_READ_DELETE)
@MsAuditLog(module = OperLogModule.TRACK_BUG, type = OperLogConstants.DELETE, beforeEvent = "#msClass.getLogDetails(#id)", msClass = IssuesService.class)
@SendNotice(taskType = NoticeConstants.TaskType.DEFECT_TASK, target = "#targetClass.getIssue(#id)", targetClass = IssuesService.class, event = NoticeConstants.Event.DELETE, subject = "缺陷通知")
public void delete(@PathVariable String id) {
issuesService.delete(id);
}
@PostMapping("/batchDelete")
@RequiresPermissions(PermissionConstants.PROJECT_TRACK_ISSUE_READ_DELETE)
@MsAuditLog(module = OperLogModule.TRACK_BUG, type = OperLogConstants.DELETE, beforeEvent = "#msClass.getLogDetails(#request)", msClass = IssuesService.class)
public void batchDelete(@RequestBody IssuesUpdateRequest request) {
issuesService.batchDelete(request);
}
@PostMapping("/tapd/user")
public List<PlatformUser> getTapdUsers(@RequestBody IssuesRequest request) {
return issuesService.getTapdProjectUsers(request);
@ -190,4 +204,23 @@ public class IssuesController {
public void checkThirdProjectExist(@RequestBody Project project) {
issuesService.checkThirdProjectExist(project);
}
@GetMapping("/import/template/download/{projectId}")
@RequiresPermissions(PermissionConstants.PROJECT_TRACK_ISSUE_READ_CREATE)
public void downloadImportTemplate(@PathVariable String projectId, HttpServletResponse response) {
issuesService.issueImportTemplate(projectId, response);
}
@PostMapping("/import")
@MsAuditLog(module = OperLogModule.TRACK_BUG, type = OperLogConstants.IMPORT, project = "#request.projectId")
public ExcelResponse issueImport(@RequestPart("request") IssueImportRequest request, @RequestPart("file") MultipartFile file) {
baseCheckPermissionService.checkProjectOwner(request.getProjectId());
return issuesService.issueImport(request, file);
}
@PostMapping("/export")
@MsAuditLog(module = OperLogModule.TRACK_BUG, type = OperLogConstants.EXPORT, project = "#exportRequest.projectId")
public void exportIssues(@RequestBody IssueExportRequest exportRequest, HttpServletResponse response) {
issuesService.issueExport(exportRequest, response);
}
}

View File

@ -0,0 +1,46 @@
package io.metersphere.excel.constants;
import io.metersphere.commons.utils.DateUtils;
import io.metersphere.excel.domain.IssueExcelData;
import io.metersphere.i18n.Translator;
import java.util.function.Function;
/**
* @author songcc
* 导出缺陷HEAD枚举
*/
public enum IssueExportHeadField {
ID("id", "ID", issueExcelData -> issueExcelData.getNum().toString()), TITLE("title", Translator.get("title"), IssueExcelData::getTitle),
CREATOR("creator", Translator.get("create_user"), IssueExcelData::getCreator),
DESCRIPTION("description", Translator.get("description"), IssueExcelData::getDescription),
CASE_COUNT("caseCount", Translator.get("case_count"), issueExcelData -> String.valueOf(issueExcelData.getCaseCount())),
COMMENT("comment", Translator.get("comment"), IssueExcelData::getComment),
RESOURCE("resource", Translator.get("issue_resource"), IssueExcelData::getResourceName),
PLATFORM("platform", Translator.get("issue_platform"), IssueExcelData::getPlatform),
CREATE_TIME("createTime", Translator.get("create_time"), issueExcelData -> issueExcelData.getCreateTime() != null ? DateUtils.getTimeStr(issueExcelData.getCreateTime()) : null);
private String id;
private String name;
private Function<IssueExcelData, String> parseFunc;
IssueExportHeadField(String id, String name, Function<IssueExcelData, String> parseFunc) {
this.id = id;
this.name = name;
this.parseFunc = parseFunc;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public String parseExcelDataValue(IssueExcelData excelData) {
return parseFunc.apply(excelData);
}
}

View File

@ -0,0 +1,41 @@
package io.metersphere.excel.constants;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public enum IssueImportField {
/**
* TITLE, DESCRIPTION, STATUS, PROCESSOR, PRIORITY
*/
TITLE("title","缺陷标题", "缺陷標題", "Title"),
DESCRIPTION("description","缺陷描述", "缺陷描述", "Description");
// STATUS("status","状态", "狀態", "Status", IssueExcelData::getStatus),
// PROCESSOR("processor","处理人", "處理人", "Processor", IssueExcelData::getProcessor),
// PRIORITY("priority","严重程度", "嚴重程度", "Priority", IssueExcelData::getPriority);
private Map<Locale, String> fieldLangMap;
private String value;
IssueImportField(String value, String zn, String chineseTw, String us) {
this.fieldLangMap = new HashMap<>() {{
put(Locale.SIMPLIFIED_CHINESE, zn);
put(Locale.TRADITIONAL_CHINESE, chineseTw);
put(Locale.US, us);
}};
this.value = value;
}
public Map<Locale, String> getFieldLangMap() {
return this.fieldLangMap;
}
public String getValue() {
return value;
}
public boolean containsHead(String head) {
return this.fieldLangMap.containsValue(head);
}
}

View File

@ -0,0 +1,114 @@
package io.metersphere.excel.domain;
import com.alibaba.excel.annotation.ExcelIgnore;
import io.metersphere.dto.CustomFieldDao;
import io.metersphere.excel.constants.IssueExportHeadField;
import io.metersphere.excel.constants.IssueImportField;
import io.metersphere.request.issues.IssueExportRequest;
import lombok.Data;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
import java.util.*;
@Data
public class IssueExcelData implements Serializable {
@ExcelIgnore
private String id;
@ExcelIgnore
private Integer num;
@ExcelIgnore
private String platform;
@ExcelIgnore
private String creator;
@ExcelIgnore
private long caseCount;
@ExcelIgnore
private Long createTime;
@ExcelIgnore
private Long updateTime;
@ExcelIgnore
private String reporter;
@ExcelIgnore
private String projectId;
@ExcelIgnore
private String platformId;
@ExcelIgnore
private String platformStatus;
@ExcelIgnore
private String resourceId;
@ExcelIgnore
private String resourceName;
@ExcelIgnore
private String customFields;
@ExcelIgnore
private String comment;
@ExcelIgnore
private Boolean addFlag;
@ExcelIgnore
private String title;
@ExcelIgnore
private String description;
/**
* 处理人
* 状态
* 严重程度
*/
@ExcelIgnore
private String processor;
@ExcelIgnore
private String status;
@ExcelIgnore
private String priority;
@ExcelIgnore
Map<String, Object> customData = new LinkedHashMap<>();
public List<List<String>> getHead(Boolean isThirdTemplate, List<CustomFieldDao> customFields, IssueExportRequest request) {
return new ArrayList<>();
}
public List<List<String>> getHead(Boolean isThirdTemplate, List<CustomFieldDao> customFields, IssueExportRequest request, Locale lang) {
List<List<String>> heads = new ArrayList<>();
IssueExportHeadField[] exportHeadFields = IssueExportHeadField.values();
if (request != null) {
List<IssueExportRequest.IssueExportHeader> baseHeaders = request.getExportFields().get("baseHeaders");
List<IssueExportRequest.IssueExportHeader> customHeaders = request.getExportFields().get("customHeaders");
List<IssueExportRequest.IssueExportHeader> otherHeaders = request.getExportFields().get("otherHeaders");
baseHeaders.forEach(baseHeader -> {
for (IssueExportHeadField exportHeadField : exportHeadFields) {
if (StringUtils.equals(baseHeader.getId(), exportHeadField.getId())) {
heads.add(List.of(exportHeadField.getName()));
}
}
});
customHeaders.forEach(customHeader -> {
heads.add(List.of(customHeader.getName()));
});
otherHeaders.forEach(otherHeader -> {
for (IssueExportHeadField exportHeadField : exportHeadFields) {
if (StringUtils.equals(otherHeader.getId(), exportHeadField.getId())) {
heads.add(List.of(exportHeadField.getName()));
}
}
});
} else {
if (!isThirdTemplate) {
IssueImportField[] fields = IssueImportField.values();
for (IssueImportField field : fields) {
heads.add(List.of(field.getFieldLangMap().get(lang)));
}
}
if (CollectionUtils.isNotEmpty(customFields)) {
for (CustomFieldDao dto : customFields) {
heads.add(new ArrayList<>() {{
add(dto.getName());
}});
}
}
}
return heads;
}
}

View File

@ -0,0 +1,33 @@
package io.metersphere.excel.domain;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import io.metersphere.dto.CustomFieldDao;
import io.metersphere.request.issues.IssueExportRequest;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import java.util.List;
import java.util.Locale;
@Data
public class IssueExcelDataCn extends IssueExcelData{
@NotBlank(message = "{cannot_be_null}")
@Length(max = 255)
@ColumnWidth(50)
@ExcelProperty("缺陷标题")
private String title;
@NotBlank(message = "{cannot_be_null}")
@Length(max = 1000)
@ColumnWidth(50)
@ExcelProperty("缺陷描述")
private String description;
@Override
public List<List<String>> getHead(Boolean isThirdTemplate, List<CustomFieldDao> customFields, IssueExportRequest request) {
return super.getHead(isThirdTemplate, customFields, request, Locale.SIMPLIFIED_CHINESE);
}
}

View File

@ -0,0 +1,30 @@
package io.metersphere.excel.domain;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.i18n.LocaleContextHolder;
import java.util.Locale;
public class IssueExcelDataFactory implements ExcelDataFactory{
@Override
public Class getExcelDataByLocal() {
Locale locale = LocaleContextHolder.getLocale();
if (StringUtils.equals(locale.toString(), Locale.US.toString())) {
return IssueExcelDataUs.class;
} else if (StringUtils.equals(locale.toString(), Locale.TRADITIONAL_CHINESE.toString())) {
return IssueExcelDataTw.class;
}
return IssueExcelDataCn.class;
}
public IssueExcelData getIssueExcelDataLocal(){
Locale locale = LocaleContextHolder.getLocale();
if (StringUtils.equals(locale.toString(), Locale.US.toString())) {
return new IssueExcelDataUs();
} else if (StringUtils.equals(locale.toString(), Locale.TRADITIONAL_CHINESE.toString())) {
return new IssueExcelDataTw();
}
return new IssueExcelDataCn();
}
}

View File

@ -0,0 +1,33 @@
package io.metersphere.excel.domain;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import io.metersphere.dto.CustomFieldDao;
import io.metersphere.request.issues.IssueExportRequest;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import java.util.List;
import java.util.Locale;
@Data
public class IssueExcelDataTw extends IssueExcelData{
@NotBlank(message = "{cannot_be_null}")
@Length(max = 255)
@ColumnWidth(100)
@ExcelProperty("缺陷標題")
private String title;
@NotBlank(message = "{cannot_be_null}")
@Length(max = 1000)
@ColumnWidth(100)
@ExcelProperty("缺陷内容")
private String description;
@Override
public List<List<String>> getHead(Boolean isThirdTemplate, List<CustomFieldDao> customFields, IssueExportRequest request) {
return super.getHead(isThirdTemplate, customFields, request, Locale.TRADITIONAL_CHINESE);
}
}

View File

@ -0,0 +1,34 @@
package io.metersphere.excel.domain;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import io.metersphere.dto.CustomFieldDao;
import io.metersphere.request.issues.IssueExportRequest;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import java.util.List;
import java.util.Locale;
@Data
public class IssueExcelDataUs extends IssueExcelData{
@NotBlank(message = "{cannot_be_null}")
@Length(max = 255)
@ColumnWidth(100)
@ExcelProperty("Title")
private String title;
@NotBlank(message = "{cannot_be_null}")
@Length(max = 1000)
@ColumnWidth(100)
@ExcelProperty("Description")
private String description;
@Override
public List<List<String>> getHead(Boolean isThirdTemplate, List<CustomFieldDao> customFields, IssueExportRequest request) {
return super.getHead(isThirdTemplate, customFields, request, Locale.US);
}
}

View File

@ -0,0 +1,40 @@
package io.metersphere.excel.handler;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
import org.apache.poi.ss.usermodel.DataValidationHelper;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddressList;
import java.util.Map;
public class IssueTemplateCellWriteHandler implements SheetWriteHandler {
private static final Integer ROW_SIZE = 200;
/**
* 下拉框集合
*/
private Map<Integer, String[]> dropDownIndexMap;
public IssueTemplateCellWriteHandler(Map<Integer, String[]> dropDownIndexMap) {
this.dropDownIndexMap = dropDownIndexMap;
}
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
Sheet sheet = writeSheetHolder.getSheet();
DataValidationHelper helper = sheet.getDataValidationHelper();
dropDownIndexMap.forEach((celIndex, options) -> {
// 区间设置
CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(1, ROW_SIZE, celIndex, celIndex);
// 下拉内容
DataValidationConstraint constraint = helper.createExplicitListConstraint(options);
DataValidation dataValidation = helper.createValidation(constraint, cellRangeAddressList);
sheet.addValidationData(dataValidation);
});
}
}

View File

@ -0,0 +1,183 @@
package io.metersphere.excel.handler;
import com.alibaba.excel.util.BooleanUtils;
import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.handler.context.RowWriteHandlerContext;
import io.metersphere.commons.constants.CustomFieldScene;
import io.metersphere.commons.constants.CustomFieldType;
import io.metersphere.commons.utils.JSON;
import io.metersphere.dto.CustomFieldDao;
import io.metersphere.dto.CustomFieldOptionDTO;
import io.metersphere.excel.constants.IssueExportHeadField;
import io.metersphere.i18n.Translator;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.Comment;
import org.apache.poi.ss.usermodel.Drawing;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.stream.Collectors;
/**
* 表头, 单元格后置处理
*/
public class IssueTemplateHeadWriteHandler implements RowWriteHandler, SheetWriteHandler {
private Sheet sheet;
private Drawing<?> drawingPatriarch;
private Map<String, String> memberMap;
private Map<Integer, String> headCommentIndexMap = new HashMap<>();
public IssueTemplateHeadWriteHandler(Map<String, String> memberMap, List<List<String>> headList, List<CustomFieldDao> customFields) {
this.memberMap = memberMap;
this.initCustomFieldIndexMap(headList, customFields);
}
private void initCustomFieldIndexMap(List<List<String>> headList, List<CustomFieldDao> customFields) {
int index = 0;
for (List<String> list : headList) {
for (String head : list) {
CustomFieldDao customFieldDao;
if (StringUtils.equalsAnyIgnoreCase(head, IssueExportHeadField.TITLE.getName(), IssueExportHeadField.DESCRIPTION.getName())) {
customFieldDao = new CustomFieldDao();
customFieldDao.setRequired(Boolean.TRUE);
customFieldDao.setType(CustomFieldType.INPUT.getValue());
} else if (StringUtils.equalsAnyIgnoreCase(head, IssueExportHeadField.ID.getName())) {
customFieldDao = new CustomFieldDao();
customFieldDao.setRequired(Boolean.FALSE);
customFieldDao.setType(CustomFieldType.INT.getValue());
} else if (StringUtils.equalsAnyIgnoreCase(head, IssueExportHeadField.CREATOR.getName())) {
customFieldDao = new CustomFieldDao();
customFieldDao.setRequired(Boolean.FALSE);
customFieldDao.setType(CustomFieldType.MEMBER.getValue());
} else {
// 自定义字段
List<CustomFieldDao> fields = customFields.stream().filter(field -> StringUtils.equals(field.getName(), head)).collect(Collectors.toList());
if (fields.size() > 0) {
customFieldDao = fields.get(0);
} else {
customFieldDao = null;
}
}
headCommentIndexMap.put(index, customFieldDao == null ? StringUtils.EMPTY : getCommentByCustomField(customFieldDao));
index++;
}
}
}
@Override
public void afterRowDispose(RowWriteHandlerContext context) {
if (BooleanUtils.isTrue(context.getHead())) {
sheet = context.getWriteSheetHolder().getSheet();
drawingPatriarch = sheet.createDrawingPatriarch();
headCommentIndexMap.forEach(this::setComment);
}
}
private String getCommentByCustomField(CustomFieldDao field) {
String commentText = "";
if (StringUtils.equalsAnyIgnoreCase(field.getType(),
CustomFieldType.SELECT.getValue(), CustomFieldType.RADIO.getValue())) {
if (StringUtils.equalsAnyIgnoreCase(field.getScene(), CustomFieldScene.ISSUE.name()) &&
StringUtils.equalsAnyIgnoreCase(field.getName(), "状态", "严重程度")) {
commentText = Translator.get("options").concat(JSON.toJSONString(getOptionValues(field)));
} else {
commentText = Translator.get("options_tips").concat(JSON.toJSONString(getOptionsText(field.getOptions())));
}
}
if (StringUtils.equalsAnyIgnoreCase(field.getType(), CustomFieldType.MEMBER.getValue())) {
commentText = Translator.get("options").concat(memberMap.keySet().toString());
}
if (StringUtils.equalsAnyIgnoreCase(field.getType(), CustomFieldType.DATE.getValue())) {
commentText = Translator.get("date_import_cell_format_comment");
}
if (StringUtils.equalsAnyIgnoreCase(field.getType(), CustomFieldType.DATETIME.getValue())) {
commentText = Translator.get("datetime_import_cell_format_comment");
}
if (StringUtils.equalsAnyIgnoreCase(field.getType(), CustomFieldType.INT.getValue())) {
commentText = Translator.get("int_import_cell_format_comment");
}
if (StringUtils.equalsAnyIgnoreCase(field.getType(), CustomFieldType.FLOAT.getValue())) {
commentText = Translator.get("float_import_cell_format_comment");
}
if (StringUtils.equalsAnyIgnoreCase(field.getType(), CustomFieldType.MULTIPLE_INPUT.getValue())) {
commentText = Translator.get("multiple_input_import_cell_format_comment");
}
if (StringUtils.equalsAnyIgnoreCase(field.getType(),
CustomFieldType.MULTIPLE_SELECT.getValue(), CustomFieldType.CHECKBOX.getValue())) {
commentText = Translator.get("multiple_input_import_cell_format_comment").concat(", " +
Translator.get("options_tips").concat(JSON.toJSONString(getOptionsText(field.getOptions()))));
}
if (StringUtils.equalsAnyIgnoreCase(field.getType(), CustomFieldType.MULTIPLE_MEMBER.getValue())) {
commentText = Translator.get("multiple_input_import_cell_format_comment").concat(", " +
Translator.get("options").concat(memberMap.keySet().toString()));
}
if (StringUtils.equalsAnyIgnoreCase(field.getType(), CustomFieldType.CASCADING_SELECT.getValue())) {
commentText = Translator.get("multiple_input_import_cell_format_comment").concat(", " +
Translator.get("options_tips").concat(JSON.toJSONString(getCascadSelect(field.getOptions()))));
}
return field.getRequired() ? Translator.get("required").concat("; " + commentText) : commentText;
}
private void setComment(Integer index, String text) {
if (index == null || StringUtils.isEmpty(text)) {
return;
}
Comment comment = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, index, 0, index + 3, 1));
comment.setString(new XSSFRichTextString(text));
sheet.getRow(0).getCell(1).setCellComment(comment);
}
@NotNull
private List<String> getOptionValues(CustomFieldDao field) {
List<CustomFieldOptionDTO> options = JSON.parseArray(field.getOptions(), CustomFieldOptionDTO.class);
List<String> values = new ArrayList<>();
if (options != null) {
for (CustomFieldOptionDTO option : options) {
values.add(option.getValue());
}
}
return values;
}
private List<String> getOptionsText(String optionStr) {
if (StringUtils.isEmpty(optionStr)) {
return Collections.emptyList();
}
List<String> options = new ArrayList<>();
List<Map> optionMapList = JSON.parseArray(optionStr, Map.class);
optionMapList.forEach(optionMap -> {
StringBuffer option = new StringBuffer("{" + optionMap.get("text") + ":" + optionMap.get("value") + "}");
options.add(option.toString());
});
return options;
}
private List<String> getCascadSelect(String optionStr) {
if (StringUtils.isEmpty(optionStr)) {
return Collections.emptyList();
}
List<String> options = new ArrayList<>();
List<Map> optionMapList = JSON.parseArray(optionStr, Map.class);
optionMapList.forEach(optionMap -> {
StringBuffer option = new StringBuffer();
if (optionMap.get("children") != null) {
String children = getCascadSelect(JSON.toJSONString(optionMap.get("children"))).toString();
option.append("{").append(optionMap.get("text"))
.append(":").append(optionMap.get("value")).append(":").append(children).append("}");
} else {
option.append("{").append(optionMap.get("text"))
.append(":").append(optionMap.get("value")).append("}");
}
options.add(option.toString());
});
return options;
}
}

View File

@ -0,0 +1,356 @@
package io.metersphere.excel.listener;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.util.DateUtils;
import io.metersphere.base.domain.Issues;
import io.metersphere.commons.constants.CustomFieldType;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.dto.CustomFieldDao;
import io.metersphere.dto.CustomFieldItemDTO;
import io.metersphere.dto.CustomFieldResourceDTO;
import io.metersphere.excel.constants.IssueExportHeadField;
import io.metersphere.excel.domain.ExcelErrData;
import io.metersphere.excel.domain.IssueExcelData;
import io.metersphere.excel.domain.IssueExcelDataFactory;
import io.metersphere.excel.utils.ExcelImportType;
import io.metersphere.excel.utils.ExcelValidateHelper;
import io.metersphere.i18n.Translator;
import io.metersphere.request.issues.IssueImportRequest;
import io.metersphere.service.IssuesService;
import io.metersphere.xpack.track.dto.request.IssuesUpdateRequest;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
/**
* 缺陷导入读取
* @author songcc
*/
public class IssueExcelListener extends AnalysisEventListener<Map<Integer, String>> {
private Class dataClass;
private IssueImportRequest request;
private Boolean isThirdPlatform = false;
private Map<Integer, String> headMap;
private List<CustomFieldDao> customFields = new ArrayList<>();
private IssuesService issuesService;
/**
* excel表头字段字典值
*/
private Map<String, String> headFieldTransferDic = new HashMap<>();
private Map<String, List<CustomFieldResourceDTO>> issueCustomFieldMap = new HashMap<>();
/**
* 每超过2000条数据, 则插入数据库
*/
protected static final int BATCH_THRESHOLD = 2000;
/**
* insertList: 新增缺陷集合
* updateList: 覆盖缺陷集合
* errList: 校验失败缺陷集合
*/
protected List<IssueExcelData> insertList = new ArrayList<>();
protected List<IssueExcelData> updateList = new ArrayList<>();
protected List<ExcelErrData<IssueExcelData>> errList = new ArrayList<>();
public IssueExcelListener(IssueImportRequest request, Class clazz, Boolean isThirdPlatform, List<CustomFieldDao> customFields) {
this.request = request;
this.dataClass = clazz;
this.isThirdPlatform = isThirdPlatform;
this.customFields = customFields;
this.issuesService = CommonBeanFactory.getBean(IssuesService.class);
}
@Override
public void invoke(Map<Integer, String> data, AnalysisContext analysisContext) {
Integer rowIndex = analysisContext.readRowHolder().getRowIndex();
IssueExcelData issueExcelData = null;
StringBuilder errMsg;
try {
issueExcelData = this.parseDataToModel(data);
// EXCEL校验, 如果不是第三方模板则需要校验
errMsg = new StringBuilder(!isThirdPlatform ? ExcelValidateHelper.validateEntity(issueExcelData) : StringUtils.EMPTY);
//自定义校验规则
if (StringUtils.isEmpty(errMsg)) {
validate(issueExcelData, errMsg);
}
} catch (Exception e) {
errMsg = new StringBuilder(Translator.get("parse_data_error"));
LogUtil.error(e.getMessage(), e);
}
if (StringUtils.isNotEmpty(errMsg)) {
ExcelErrData excelErrData = new ExcelErrData(issueExcelData, rowIndex,
Translator.get("number")
.concat(StringUtils.SPACE)
.concat(String.valueOf(rowIndex + 1)).concat(StringUtils.SPACE)
.concat(Translator.get("row"))
.concat(Translator.get("error"))
.concat("")
.concat(errMsg.toString()));
errList.add(excelErrData);
} else {
if (issueExcelData.getNum() == null) {
// ID为空或不存在, 新增
issueExcelData.setAddFlag(Boolean.TRUE);
insertList.add(issueExcelData);
} else {
Issues issues = checkIssueExist(issueExcelData.getNum(), request.getProjectId());
if (issues == null) {
// ID列值不存在, 则新增
issueExcelData.setAddFlag(Boolean.TRUE);
insertList.add(issueExcelData);
} else {
// ID存在
if (StringUtils.equals(request.getImportType(), ExcelImportType.Update.name())) {
// 覆盖模式
issueExcelData.setId(issues.getId());
issueExcelData.setAddFlag(Boolean.FALSE);
updateList.add(issueExcelData);
}
}
}
}
if (insertList.size() > BATCH_THRESHOLD || updateList.size() > BATCH_THRESHOLD) {
saveData();
insertList.clear();
updateList.clear();
}
}
public void saveData() {
//excel中用例都有错误时就返回只要有用例可用于更新或者插入就不返回
if (!errList.isEmpty()) {
return;
}
if (CollectionUtils.isEmpty(insertList) && CollectionUtils.isEmpty(updateList)) {
MSException.throwException(Translator.get("no_legitimate_issue_tip"));
}
if (CollectionUtils.isNotEmpty(insertList)) {
List<IssuesUpdateRequest> issues = insertList.stream().map(item -> this.convertToIssue(item)).collect(Collectors.toList());
issuesService.saveImportData(issues);
}
if (CollectionUtils.isNotEmpty(updateList)) {
List<IssuesUpdateRequest> issues = updateList.stream().map(item -> this.convertToIssue(item)).collect(Collectors.toList());
issuesService.updateImportData(issues);
}
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
this.headMap = headMap;
this.genExcelHeadFieldTransferDic();
this.formatHeadMap();
super.invokeHeadMap(headMap, context);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
saveData();
insertList.clear();
updateList.clear();
issueCustomFieldMap.clear();
}
private void formatHeadMap() {
for (Integer key : headMap.keySet()) {
String name = headMap.get(key);
if (headFieldTransferDic.containsKey(name)) {
headMap.put(key, headFieldTransferDic.get(name));
}
}
}
public void validate(IssueExcelData data, StringBuilder errMsg) {
// TODO 校验自定义字段的数据是否合法
}
private IssueExcelData parseDataToModel(Map<Integer, String> rowData) {
IssueExcelData data = new IssueExcelDataFactory().getIssueExcelDataLocal();
for (Map.Entry<Integer, String> headEntry : headMap.entrySet()) {
Integer index = headEntry.getKey();
String field = headEntry.getValue();
if (StringUtils.isBlank(field)) {
continue;
}
String value = StringUtils.isEmpty(rowData.get(index)) ? StringUtils.EMPTY : rowData.get(index);
if (StringUtils.equalsIgnoreCase(field, IssueExportHeadField.ID.getName())) {
data.setNum(StringUtils.isEmpty(value) ? null : Integer.parseInt(value));
} else if (StringUtils.equalsAnyIgnoreCase(field, IssueExportHeadField.TITLE.getId())) {
data.setTitle(value);
} else if (StringUtils.equalsAnyIgnoreCase(field, IssueExportHeadField.DESCRIPTION.getId())) {
data.setDescription(value);
} else {
// 自定义字段
if (StringUtils.isNotEmpty(value) && (value.contains(","))) {
// 逗号分隔
List<String> dataList = Arrays.asList(org.springframework.util.StringUtils.trimAllWhitespace(value).split(","));
List<String> formatDataList = dataList.stream().map(item -> "\"" + item + "\"").collect(Collectors.toList());
data.getCustomData().put(field, formatDataList);
} else if (StringUtils.isNotEmpty(value) && (value.contains(";"))){
// 分号分隔
List<String> dataList = Arrays.asList(org.springframework.util.StringUtils.trimAllWhitespace(value).split(";"));
List<String> formatDataList = dataList.stream().map(item -> "\"" + item + "\"").collect(Collectors.toList());
data.getCustomData().put(field, formatDataList);
} else {
data.getCustomData().put(field, value);
}
}
}
return data;
}
private IssuesUpdateRequest convertToIssue(IssueExcelData issueExcelData) {
IssuesUpdateRequest issuesUpdateRequest = new IssuesUpdateRequest();
issuesUpdateRequest.setWorkspaceId(request.getWorkspaceId());
issuesUpdateRequest.setProjectId(request.getProjectId());
issuesUpdateRequest.setThirdPartPlatform(isThirdPlatform);
issuesUpdateRequest.setDescription(issueExcelData.getDescription());
issuesUpdateRequest.setTitle(issueExcelData.getTitle());
if (BooleanUtils.isTrue(issueExcelData.getAddFlag())) {
issuesUpdateRequest.setCreator(SessionUtils.getUserId());
} else {
issuesUpdateRequest.setPlatformId(getPlatformId(issueExcelData.getId()));
issuesUpdateRequest.setId(issueExcelData.getId());
}
buildFields(issueExcelData, issuesUpdateRequest);
return issuesUpdateRequest;
}
public List<ExcelErrData<IssueExcelData>> getErrList() {
return this.errList;
}
/**
* 获取注解ExcelProperty的value和对应field
*/
public void genExcelHeadFieldTransferDic() {
Field[] fields = dataClass.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
if (excelProperty != null) {
StringBuilder value = new StringBuilder();
for (String v : excelProperty.value()) {
value.append(v);
}
headFieldTransferDic.put(value.toString(), field.getName());
}
}
}
private Issues checkIssueExist(Integer num, String projectId) {
return issuesService.checkIssueExist(num, projectId);
}
private void buildFields(IssueExcelData issueExcelData, IssuesUpdateRequest issuesUpdateRequest) {
if (MapUtils.isEmpty(issueExcelData.getCustomData())) {
return;
}
Boolean addFlag = issueExcelData.getAddFlag();
List<CustomFieldResourceDTO> addFields = new ArrayList<>();
List<CustomFieldResourceDTO> editFields = new ArrayList<>();
List<CustomFieldItemDTO> requestFields = new ArrayList<>();
Map<String, List<CustomFieldDao>> customFieldMap = customFields.stream().collect(Collectors.groupingBy(CustomFieldDao::getName));
issueExcelData.getCustomData().forEach((k, v) -> {
try {
List<CustomFieldDao> customFieldDaos = customFieldMap.get(k);
if (CollectionUtils.isNotEmpty(customFieldDaos) && customFieldDaos.size() > 0) {
CustomFieldDao customFieldDao = customFieldDaos.get(0);
String type = customFieldDao.getType();
// addfield
CustomFieldResourceDTO customFieldResourceDTO = new CustomFieldResourceDTO();
customFieldResourceDTO.setFieldId(customFieldDao.getId());
// requestfield
CustomFieldItemDTO customFieldItemDTO = new CustomFieldItemDTO();
BeanUtils.copyBean(customFieldItemDTO, customFieldDao);
if (StringUtils.isEmpty(v.toString())) {
if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.MULTIPLE_MEMBER.getValue(),
CustomFieldType.MULTIPLE_SELECT.getValue(), CustomFieldType.CHECKBOX.getValue(),
CustomFieldType.CASCADING_SELECT.getValue())) {
customFieldResourceDTO.setValue("[]");
customFieldItemDTO.setValue("[]");
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.MULTIPLE_INPUT.getValue())) {
customFieldResourceDTO.setValue("[]");
customFieldItemDTO.setValue(Collections.emptyList());
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.RADIO.getValue(),
CustomFieldType.RICH_TEXT.getValue(), CustomFieldType.SELECT.getValue(),
CustomFieldType.FLOAT.getValue(), CustomFieldType.DATE.getValue(),
CustomFieldType.DATETIME.getValue(), CustomFieldType.INPUT.getValue())) {
customFieldResourceDTO.setValue(StringUtils.EMPTY);
customFieldItemDTO.setValue(StringUtils.EMPTY);
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.TEXTAREA.getValue())) {
customFieldItemDTO.setValue(StringUtils.EMPTY);
}
} else {
if (StringUtils.equalsAnyIgnoreCase(type,
CustomFieldType.RICH_TEXT.getValue(), CustomFieldType.TEXTAREA.getValue())) {
customFieldResourceDTO.setTextValue(v.toString());
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.FLOAT.getValue())) {
customFieldResourceDTO.setValue(v.toString());
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.MULTIPLE_SELECT.getValue(),
CustomFieldType.CHECKBOX.getValue(), CustomFieldType.MULTIPLE_INPUT.getValue(),
CustomFieldType.MULTIPLE_MEMBER.getValue(), CustomFieldType.CASCADING_SELECT.getValue())) {
if (!v.toString().contains("[")) {
v = List.of("\"" + v.toString() + "\"");
}
customFieldResourceDTO.setValue(v.toString());
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.DATE.getValue())) {
Date vdate = DateUtils.parseDate(v.toString(), "yyyy/MM/dd");
v = DateUtils.format(vdate, "yyyy-MM-dd");
customFieldResourceDTO.setValue("\"" + v + "\"");
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.DATETIME.getValue())) {
Date vdate = DateUtils.parseDate(v.toString());
v = DateUtils.format(vdate, "yyyy-MM-dd'T'HH:mm");
customFieldResourceDTO.setValue("\"" + v + "\"");
} else {
customFieldResourceDTO.setValue("\"" + v + "\"");
}
}
if (addFlag) {
addFields.add(customFieldResourceDTO);
} else {
editFields.add(customFieldResourceDTO);
}
if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.MULTIPLE_INPUT.getValue())) {
customFieldItemDTO.setValue(v);
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.FLOAT.getValue())) {
customFieldItemDTO.setValue(StringUtils.isNotEmpty(v.toString()) ? Float.parseFloat(v.toString()) : StringUtils.EMPTY);
} else {
customFieldItemDTO.setValue(v.toString());
}
requestFields.add(customFieldItemDTO);
}
} catch (Exception e) {
MSException.throwException(e.getMessage());
}
});
if (addFlag) {
issuesUpdateRequest.setAddFields(addFields);
} else {
issuesUpdateRequest.setEditFields(editFields);
}
issuesUpdateRequest.setRequestFields(requestFields);
}
private String getPlatformId(String issueId) {
return issuesService.getIssue(issueId).getPlatformId();
}
}

View File

@ -0,0 +1,31 @@
package io.metersphere.request.issues;
import io.metersphere.request.OrderRequest;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Map;
/**
* @author songcc
* 缺陷导出参数
*/
@Data
public class IssueExportRequest {
private String projectId;
private String workspaceId;
private String userId;
private Boolean isSelectAll;
private List<String> exportIds;
private List<OrderRequest> orders;
private Map<String, List<IssueExportHeader>> exportFields;
@Getter
@Setter
public static class IssueExportHeader {
private String id;
private String name;
}
}

View File

@ -0,0 +1,11 @@
package io.metersphere.request.issues;
import lombok.Data;
@Data
public class IssueImportRequest {
private String projectId;
private String workspaceId;
private String userId;
private String importType;
}

View File

@ -1,48 +1,56 @@
package io.metersphere.service;
import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.util.DateUtils;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.*;
import io.metersphere.base.mapper.ext.ExtIssueCommentMapper;
import io.metersphere.base.mapper.ext.ExtIssuesMapper;
import io.metersphere.commons.constants.FileAssociationType;
import io.metersphere.commons.constants.IssueRefType;
import io.metersphere.commons.constants.IssuesManagePlatform;
import io.metersphere.commons.constants.IssuesStatus;
import io.metersphere.commons.constants.*;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.*;
import io.metersphere.plan.service.TestPlanTestCaseService;
import io.metersphere.utils.DistinctKeyUtil;
import io.metersphere.xpack.track.dto.AttachmentSyncType;
import io.metersphere.xpack.track.dto.*;
import io.metersphere.constants.AttachmentType;
import io.metersphere.constants.SystemCustomField;
import io.metersphere.dto.CustomFieldDao;
import io.metersphere.dto.*;
import io.metersphere.excel.constants.IssueExportHeadField;
import io.metersphere.excel.domain.ExcelErrData;
import io.metersphere.excel.domain.ExcelResponse;
import io.metersphere.excel.domain.IssueExcelData;
import io.metersphere.excel.domain.IssueExcelDataFactory;
import io.metersphere.excel.handler.IssueTemplateHeadWriteHandler;
import io.metersphere.excel.listener.IssueExcelListener;
import io.metersphere.excel.utils.EasyExcelExporter;
import io.metersphere.i18n.Translator;
import io.metersphere.log.utils.ReflexObjectUtil;
import io.metersphere.log.vo.DetailColumn;
import io.metersphere.log.vo.OperatingLogDetails;
import io.metersphere.log.vo.track.TestPlanReference;
import io.metersphere.dto.*;
import io.metersphere.plan.dto.PlanReportIssueDTO;
import io.metersphere.plan.dto.TestCaseReportStatusResultDTO;
import io.metersphere.plan.dto.TestPlanSimpleReportDTO;
import io.metersphere.plan.service.TestPlanService;
import io.metersphere.plan.service.TestPlanTestCaseService;
import io.metersphere.plan.utils.TestPlanStatusCalculator;
import io.metersphere.request.IntegrationRequest;
import io.metersphere.request.attachment.AttachmentRequest;
import io.metersphere.request.issues.IssueExportRequest;
import io.metersphere.request.issues.IssueImportRequest;
import io.metersphere.request.issues.JiraIssueTypeRequest;
import io.metersphere.request.issues.PlatformIssueTypeRequest;
import io.metersphere.request.testcase.AuthUserIssueRequest;
import io.metersphere.request.testcase.IssuesCountRequest;
import io.metersphere.service.issue.domain.jira.JiraIssueType;
import io.metersphere.service.issue.domain.zentao.ZentaoBuild;
import io.metersphere.request.attachment.AttachmentRequest;
import io.metersphere.xpack.track.dto.request.IssuesRequest;
import io.metersphere.xpack.track.dto.request.IssuesUpdateRequest;
import io.metersphere.service.issue.platform.*;
import io.metersphere.service.remote.project.TrackCustomFieldTemplateService;
import io.metersphere.service.remote.project.TrackIssueTemplateService;
import io.metersphere.service.wapper.TrackProjectService;
import io.metersphere.utils.DistinctKeyUtil;
import io.metersphere.xpack.track.dto.*;
import io.metersphere.xpack.track.dto.request.IssuesRequest;
import io.metersphere.xpack.track.dto.request.IssuesUpdateRequest;
import io.metersphere.xpack.track.issue.IssuesPlatform;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
@ -59,7 +67,9 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
@ -74,6 +84,8 @@ public class IssuesService {
@Resource
private TrackProjectService trackProjectService;
@Resource
private BaseUserService baseUserService;
@Resource
private BaseProjectService baseProjectService;
@Resource
private TestPlanService testPlanService;
@ -117,6 +129,8 @@ public class IssuesService {
SqlSessionFactory sqlSessionFactory;
@Resource
private FileMetadataMapper fileMetadataMapper;
@Resource
private ExtIssueCommentMapper extIssueCommentMapper;
private static final String SYNC_THIRD_PARTY_ISSUES_KEY = "ISSUE:SYNC";
@ -474,6 +488,24 @@ public class IssuesService {
attachmentService.deleteAttachment(request);
}
public void batchDelete(IssuesUpdateRequest request) {
if (request.getBatchDeleteAll()) {
IssuesRequest issuesRequest = new IssuesRequest();
issuesRequest.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
issuesRequest.setProjectId(SessionUtils.getCurrentProjectId());
List<IssuesDao> issuesDaos = listByWorkspaceId(issuesRequest);
if (CollectionUtils.isNotEmpty(issuesDaos)) {
issuesDaos.forEach(issuesDao -> {
delete(issuesDao.getId());
});
}
} else {
if (CollectionUtils.isNotEmpty(request.getBatchDeleteIds())) {
request.getBatchDeleteIds().forEach(id -> delete(id));
}
}
}
public List<ZentaoBuild> getZentaoBuilds(IssuesRequest request) {
try {
ZentaoPlatform platform = (ZentaoPlatform) IssueFactory.createPlatform(IssuesManagePlatform.Zentao.name(), request);
@ -491,7 +523,7 @@ public class IssuesService {
request.getOrders().forEach(order -> {
if (StringUtils.isNotEmpty(order.getName()) && order.getName().startsWith("custom")) {
request.setIsCustomSorted(true);
request.setCustomFieldId(order.getName().substring(order.getName().indexOf("-") + 1));
request.setCustomFieldId(order.getName().replace("custom_", StringUtils.EMPTY));
order.setPrefix("cfi");
order.setName("value");
}
@ -550,6 +582,62 @@ public class IssuesService {
data.setFields(fields);
}
private void buildCustomField(List<IssuesDao> data, Boolean isThirdTemplate, List<CustomFieldDao> customFields) {
if (CollectionUtils.isEmpty(data)) {
return;
}
Map<String, List<CustomFieldDao>> fieldMap =
customFieldIssuesService.getMapByResourceIds(data.stream().map(IssuesDao::getId).collect(Collectors.toList()));
try {
Map<String, CustomField> fieldMaps = new HashMap<>();
if (isThirdTemplate) {
fieldMaps = customFields.stream().collect(Collectors.toMap(CustomFieldDao::getId, field -> (CustomField) field));
} else {
List<CustomFieldDao> customfields = fieldMap.get(data.get(0).getId());
if (CollectionUtils.isNotEmpty(customfields) && customfields.size() > 0) {
List<String> ids = customfields.stream().map(CustomFieldDao::getId).collect(Collectors.toList());
List<CustomField> issueFields = baseCustomFieldService.getFieldByIds(ids);
fieldMaps = issueFields.stream().collect(Collectors.toMap(CustomField::getId, field -> field));
}
}
for (Map.Entry<String, List<CustomFieldDao>> entry : fieldMap.entrySet()) {
for (CustomFieldDao fieldDao : entry.getValue()) {
CustomField customField = fieldMaps.get(fieldDao.getId());
if (customField != null) {
fieldDao.setName(customField.getName());
if (StringUtils.equalsAnyIgnoreCase(customField.getType(), CustomFieldType.RICH_TEXT.getValue(), CustomFieldType.TEXTAREA.getValue())) {
fieldDao.setValue(fieldDao.getTextValue());
}
if (StringUtils.equalsAnyIgnoreCase(customField.getType(), CustomFieldType.DATE.getValue()) && StringUtils.isNotEmpty(fieldDao.getValue())) {
Date date = DateUtils.parseDate(fieldDao.getValue().replaceAll("\"", StringUtils.EMPTY), "yyyy-MM-dd");
String format = DateUtils.format(date, "yyyy/MM/dd");
fieldDao.setValue("\"" + format + "\"");
}
if (StringUtils.equalsAnyIgnoreCase(customField.getType(), CustomFieldType.DATETIME.getValue()) && StringUtils.isNotEmpty(fieldDao.getValue())) {
Date date = null;
if (fieldDao.getValue().contains("T") && fieldDao.getValue().length() == 18) {
date = DateUtils.parseDate(fieldDao.getValue().replaceAll("\"", StringUtils.EMPTY), "yyyy-MM-dd'T'HH:mm");
} else if (fieldDao.getValue().contains("T") && fieldDao.getValue().length() == 21) {
date = DateUtils.parseDate(fieldDao.getValue().replaceAll("\"", StringUtils.EMPTY), "yyyy-MM-dd'T'HH:mm:ss");
} else {
date = DateUtils.parseDate(fieldDao.getValue().replaceAll("\"", StringUtils.EMPTY));
}
String format = DateUtils.format(date, "yyyy/MM/dd HH:mm:ss");
fieldDao.setValue("\"" + format + "\"");
}
}
}
}
data.forEach(i -> i.setFields(fieldMap.get(i.getId())));
} catch (Exception e) {
MSException.throwException(e.getMessage());
}
}
private void handleJiraIssueMdUrl(String workPlaceId, String projectId, List<IssuesDao> issues) {
issues.forEach(issue -> {
if (StringUtils.isNotEmpty(issue.getDescription()) && issue.getDescription().contains("platform=Jira&")) {
@ -576,6 +664,13 @@ public class IssuesService {
"platform=Jira&project_id=" + projectId + "&workspace_id=" + workspaceId + "&");
}
private Map<String, List<IssueCommentDTO>> getCommentMap(List<IssuesDao> issues) {
List<String> issueIds = issues.stream().map(IssuesDao::getId).collect(Collectors.toList());
List<IssueCommentDTO> comments = extIssueCommentMapper.getCommentsByIssueIds(issueIds);
Map<String, List<IssueCommentDTO>> commentMap = comments.stream().collect(Collectors.groupingBy(IssueCommentDTO::getIssueId));
return commentMap;
}
private Map<String, String> getPlanMap(List<IssuesDao> issues) {
List<String> resourceIds = issues.stream().map(IssuesDao::getResourceId)
.filter(Objects::nonNull)
@ -1097,6 +1192,179 @@ public class IssuesService {
}
}
public void issueImportTemplate(String projectId, HttpServletResponse response) {
Map<String, String> userMap = baseUserService.getProjectMemberOption(projectId).stream().collect(Collectors.toMap(User::getId, User::getName));
IssueTemplateDao issueTemplate = getIssueTemplateByProjectId(projectId);
List<CustomFieldDao> customFields = Optional.ofNullable(issueTemplate.getCustomFields()).orElse(new ArrayList<>());
List<List<String>> heads = new IssueExcelDataFactory().getIssueExcelDataLocal().getHead(issueTemplate.getIsThirdTemplate(), customFields, null);
IssueTemplateHeadWriteHandler headHandler = new IssueTemplateHeadWriteHandler(userMap, heads, issueTemplate.getCustomFields());
new EasyExcelExporter(new IssueExcelDataFactory().getExcelDataByLocal())
.exportByCustomWriteHandler(response, heads, null, Translator.get("issue_import_template_name"),
Translator.get("issue_import_template_sheet"), headHandler);
}
public ExcelResponse issueImport(IssueImportRequest request, MultipartFile importFile) {
if (importFile == null) {
MSException.throwException(Translator.get("upload_fail"));
}
IssueTemplateDao issueTemplate = getIssueTemplateByProjectId(request.getProjectId());
List<CustomFieldDao> customFields = Optional.ofNullable(issueTemplate.getCustomFields()).orElse(new ArrayList<>());
Class clazz = new IssueExcelDataFactory().getExcelDataByLocal();
IssueExcelListener issueExcelListener = new IssueExcelListener(request, clazz, issueTemplate.getIsThirdTemplate(), customFields);
try {
EasyExcelFactory.read(importFile.getInputStream(), issueExcelListener).sheet().doRead();
} catch (IOException e) {
LogUtil.error(e.getMessage(), e);
e.printStackTrace();
}
List<ExcelErrData<IssueExcelData>> errList = issueExcelListener.getErrList();
ExcelResponse excelResponse = new ExcelResponse();
if (CollectionUtils.isNotEmpty(errList)) {
excelResponse.setErrList(errList);
excelResponse.setSuccess(Boolean.FALSE);
} else {
excelResponse.setSuccess(Boolean.TRUE);
}
return excelResponse;
}
public void issueExport(IssueExportRequest request, HttpServletResponse response) {
Map<String, String> userMap = baseUserService.getProjectMemberOption(request.getProjectId()).stream().collect(Collectors.toMap(User::getId, User::getName));
IssueTemplateDao issueTemplate = getIssueTemplateByProjectId(request.getProjectId());
List<CustomFieldDao> customFields = Optional.ofNullable(issueTemplate.getCustomFields()).orElse(new ArrayList<>());
List<List<String>> heads = new IssueExcelDataFactory().getIssueExcelDataLocal().getHead(issueTemplate.getIsThirdTemplate(), customFields, request);
List<IssuesDao> exportIssues = getExportIssues(request, issueTemplate.getIsThirdTemplate(), customFields);
List<IssueExcelData> excelDataList = parseIssueDataToExcelData(exportIssues);
List<List<Object>> data = parseExcelDataToList(heads, excelDataList);
IssueTemplateHeadWriteHandler headHandler = new IssueTemplateHeadWriteHandler(userMap, heads, issueTemplate.getCustomFields());
new EasyExcelExporter(new IssueExcelDataFactory().getExcelDataByLocal())
.exportByCustomWriteHandler(response, heads, data, Translator.get("issue_list_export_excel"),
Translator.get("issue_list_export_excel_sheet"), headHandler);
}
public List<IssuesDao> getExportIssues(IssueExportRequest exportRequest, Boolean isThirdTemplate, List<CustomFieldDao> customFields) {
IssuesRequest request = new IssuesRequest();
request.setProjectId(exportRequest.getProjectId());
request.setWorkspaceId(exportRequest.getWorkspaceId());
request.setSelectAll(exportRequest.getIsSelectAll());
request.setExportIds(exportRequest.getExportIds());
request.setOrders(exportRequest.getOrders());
request.setOrders(ServiceUtils.getDefaultOrderByField(request.getOrders(), "create_time"));
request.getOrders().forEach(order -> {
if (StringUtils.isNotEmpty(order.getName()) && order.getName().startsWith("custom")) {
request.setIsCustomSorted(true);
request.setCustomFieldId(order.getName().replace("custom_", StringUtils.EMPTY));
order.setPrefix("cfi");
order.setName("value");
}
});
ServiceUtils.setBaseQueryRequestCustomMultipleFields(request);
List<IssuesDao> issues = extIssuesMapper.getIssues(request);
Map<String, Set<String>> caseSetMap = getCaseSetMap(issues);
Map<String, User> userMap = getUserMap(issues);
Map<String, String> planMap = getPlanMap(issues);
Map<String, List<IssueCommentDTO>> commentMap = getCommentMap(issues);
issues.forEach(item -> {
User createUser = userMap.get(item.getCreator());
if (createUser != null) {
item.setCreatorName(createUser.getName());
}
String resourceName = planMap.get(item.getResourceId());
if (StringUtils.isNotBlank(resourceName)) {
item.setResourceName(resourceName);
}
Set<String> caseIdSet = caseSetMap.get(item.getId());
if (caseIdSet == null) {
caseIdSet = new HashSet<>();
}
item.setCaseIds(new ArrayList<>(caseIdSet));
item.setCaseCount(caseIdSet.size());
List<IssueCommentDTO> commentDTOList = commentMap.get(item.getId());
if (CollectionUtils.isNotEmpty(commentDTOList) && commentDTOList.size() > 0) {
List<String> comments = commentDTOList.stream().map(IssueCommentDTO::getDescription).collect(Collectors.toList());
item.setComment(StringUtils.join(comments, ";"));
}
});
buildCustomField(issues, isThirdTemplate, customFields);
return issues;
}
private List<IssueExcelData> parseIssueDataToExcelData(List<IssuesDao> exportIssues) {
List<IssueExcelData> excelDataList = new ArrayList<>();
for (int i = 0; i < exportIssues.size(); i++) {
IssuesDao issuesDao = exportIssues.get(i);
IssueExcelData excelData = new IssueExcelData();
BeanUtils.copyBean(excelData, issuesDao);
buildCustomData(issuesDao, excelData);
excelDataList.add(excelData);
}
return excelDataList;
}
private void buildCustomData(IssuesDao issuesDao, IssueExcelData excelData) {
if (CollectionUtils.isNotEmpty(issuesDao.getFields())) {
Map<String, Object> customData = new LinkedHashMap<>();
issuesDao.getFields().forEach(field -> {
customData.put(field.getName(), field.getValue());
});
excelData.setCustomData(customData);
}
}
private List<List<Object>> parseExcelDataToList(List<List<String>> heads, List<IssueExcelData> excelDataList) {
List<List<Object>> result = new ArrayList<>();
IssueExportHeadField[] exportHeadFields = IssueExportHeadField.values();
//转化excel头
List<String> headList = new ArrayList<>();
for (List<String> list : heads) {
for (String head : list) {
headList.add(head);
}
}
for (IssueExcelData data : excelDataList) {
List<Object> rowData = new ArrayList<>();
Map<String, Object> customData = data.getCustomData();
for (String head : headList) {
boolean isSystemField = false;
for (IssueExportHeadField exportHeadField : exportHeadFields) {
if (StringUtils.equals(head, exportHeadField.getName())) {
rowData.add(exportHeadField.parseExcelDataValue(data));
isSystemField = true;
break;
}
}
if (!isSystemField) {
// 自定义字段
Object value = customData.get(head);
if (value == null || StringUtils.equals(value.toString(), "null")) {
value = StringUtils.EMPTY;
}
rowData.add(parseCustomFieldValue(value.toString()));
}
}
result.add(rowData);
}
return result;
}
private IssueTemplateDao getIssueTemplateByProjectId(String projectId) {
IssueTemplateDao issueTemplateDao = new IssueTemplateDao();
Project project = baseProjectService.getProjectById(projectId);
if (StringUtils.equals(project.getPlatform(), IssuesManagePlatform.Jira.name()) && project.getThirdPartTemplate()) {
// 第三方Jira平台
issueTemplateDao = getThirdPartTemplate(project.getId());
issueTemplateDao.setIsThirdTemplate(Boolean.TRUE);
} else {
issueTemplateDao = trackIssueTemplateService.getTemplate(projectId);
issueTemplateDao.setIsThirdTemplate(Boolean.FALSE);
}
return issueTemplateDao;
}
private void doCheckThirdProjectExist(AbstractIssuePlatform platform, String relateId) {
if (StringUtils.isBlank(relateId)) {
MSException.throwException(Translator.get("issue_project_not_exist"));
@ -1134,4 +1402,36 @@ public class IssuesService {
});
}
}
private String parseCustomFieldValue(String value) {
if (value.contains(",")) {
value = value.replaceAll(",", ";");
}
if (value.contains("\"")) {
value = value.replaceAll("\"", StringUtils.EMPTY);
}
if (value.contains("[") || value.contains("]")) {
value = value.replaceAll("]", StringUtils.EMPTY).replaceAll("\\[", StringUtils.EMPTY);
}
return value;
}
public Issues checkIssueExist(Integer num, String projectId) {
IssuesExample example = new IssuesExample();
example.createCriteria().andNumEqualTo(num).andProjectIdEqualTo(projectId);
List<Issues> issues = issuesMapper.selectByExample(example);
return CollectionUtils.isNotEmpty(issues) && issues.size() > 0 ? issues.get(0) : null;
}
public void saveImportData(List<IssuesUpdateRequest> issues) {
issues.forEach(issue -> {
addIssues(issue, null);
});
}
public void updateImportData(List<IssuesUpdateRequest> issues) {
issues.forEach(issue -> {
updateIssues(issue);
});
}
}

View File

@ -41,6 +41,24 @@ issues_attachment_upload_not_found=Unable to upload attachment, no associated is
# issue template copy
target_issue_template_not_checked=Cannot copy, target project not checked
source_issue_template_is_empty=Copy error, source project is empty
issue_import_template_name=Issue_Template
issue_import_template_sheet=Template
issue_list_export_excel=Issue_Data_Export
issue_list_export_excel_sheet=Data
date_import_cell_format_comment=The date cell format is YYYY/MM/DD (1999/10/01)
datetime_import_cell_format_comment=The date and time cell format is YYYY/MM/DD HH:MM:SS (1999/10/01 10:01:01)
int_import_cell_format_comment=cell format: 100001
float_import_cell_format_comment=cell format: 24
multiple_input_import_cell_format_comment=This field has multiple values. Separate multiple values with commas or semicolons
options_tips=(format{key:value}, please fill in the corresponding value)Option value:
# issue export
title==Title
description=Description
case_count=Case count
comment=Comment
issue_resource=Issue resource
issue_platform=Issue platform
create_time=CreateTime
#project
project_name_is_null=Project name cannot be null
project_name_already_exists=The project name already exists
@ -175,6 +193,7 @@ custom_field_int_tip=[%s] must be integer
custom_field_member_tip=[%s] must be current project member
custom_field_select_tip=[%s] must be %s
no_legitimate_case_tip=Import fails without legitimate use cases!
no_legitimate_issue_tip=Import fails without legitimate issues!
zentao_test_type_error=invalid Zentao request
test_case_status_prepare=Prepare

View File

@ -18,6 +18,24 @@ issues_attachment_upload_not_found=无法上传附件,未找到相关联缺陷
# issue template copy
target_issue_template_not_checked=无法复制,未选中目标项目
source_issue_template_is_empty=复制错误,源项目为空
issue_import_template_name=缺陷模版
issue_import_template_sheet=模版
issue_list_export_excel=缺陷数据导出
issue_list_export_excel_sheet=数据
date_import_cell_format_comment=日期类型单元格格式为: YYYY/MM/DD (1999/10/01)
datetime_import_cell_format_comment=日期时间类型单元格格式为: YYYY/MM/DD HH:MM:SS (1999/10/01 10:01:01)
int_import_cell_format_comment=整型单元格格式为: 100001
float_import_cell_format_comment=浮点单元格格式为: 24
multiple_input_import_cell_format_comment=该单元格可输入多个值,多个值请用逗号或分号隔开(v1;v2)
options_tips=(格式{key:value},请填写对应的value)选项:
# issue export
title=缺陷标题
description=缺陷描述
case_count=用例数
comment=评论
issue_resource=缺陷来源
issue_platform=缺陷平台
create_time=创建时间
#project
project_name_is_null=项目名称不能为空
project_name_already_exists=项目名称已存在
@ -152,6 +170,7 @@ custom_field_int_tip=[%s]必须为整型
custom_field_member_tip=[%s]必须当前项目成员
custom_field_select_tip=[%s]必须为%s
no_legitimate_case_tip=导入失败,没有合法用例!
no_legitimate_issue_tip=导入失败,没有合法缺陷!
test_case_status_prepare=未开始
test_case_status_running=进行中

View File

@ -18,6 +18,24 @@ issues_attachment_upload_not_found=無法上傳附件,未找到相關缺陷:
# issue template copy
target_issue_template_not_checked=無法複製,未選中目標項目
source_issue_template_is_empty=複製錯誤,源項目為空
issue_import_template_name=缺陷模版
issue_import_template_sheet=模版
issue_list_export_excel=缺陷數據導出
issue_list_export_excel_sheet=數據
date_import_cell_format_comment=日期單元格格式爲: YYYY/MM/DD (1999/10/01)
datetime_import_cell_format_comment=日期時間單元格格式爲: YYYY/MM/DD HH:MM:SS (1999/10/01 10:01:01)
int_import_cell_format_comment=單元格格式: 100001
float_import_cell_format_comment=單元格格式: 24
multiple_input_import_cell_format_comment=該單元格可輸入多個值,多個值請用逗號或分號隔開(v1;v2)
options_tips=(格式{key:value},請填寫對應value)選項:
# issue export
title=缺陷標題
description=缺陷描述
case_count=用例數
comment=評論
issue_resource=缺陷來源
issue_platform=缺陷平臺
create_time=創建時間
#project
project_name_is_null=項目名稱不能為空
project_name_already_exists=項目名稱已存在
@ -152,6 +170,7 @@ custom_field_int_tip=[%s]必須為整型
custom_field_member_tip=[%s]必須當前項目成員
custom_field_select_tip=[%s]必須為%s
no_legitimate_case_tip=導入失敗,沒有合法用例!
no_legitimate_issue_tip=導入失敗,沒有合法缺陷!
test_case_status_prepare=未開始

View File

@ -24,6 +24,10 @@ export function deleteIssue(id) {
return get(BASE_URL + `delete/${id}`);
}
export function batchDeleteIssue(param) {
return post(BASE_URL + `batchDelete`, param);
}
export function issueStatusChange(param) {
return post(BASE_URL + 'change/status', param);
}

View File

@ -1,30 +1,28 @@
<template>
<ms-container>
<ms-main-container>
<el-card class="table-card">
<template v-slot:header>
<ms-table-header :create-permission="['PROJECT_TRACK_ISSUE:READ+CREATE']" :condition.sync="page.condition" @search="search" @create="handleCreate"
:create-tip="$t('test_track.issue.create_issue')"
:tip="$t('commons.search_by_name_or_id')">
<template v-slot:button>
<el-tooltip v-if="isThirdPart" :content="$t('test_track.issue.update_third_party_bugs')">
<ms-table-button icon="el-icon-refresh" v-if="true"
:content="$t('test_track.issue.sync_bugs')" @click="syncIssues"/>
</el-tooltip>
<ms-table-button icon="el-icon-refresh" :content="$t('test_track.issue.sync_bugs')" v-if="isThirdPart && hasPermission('PROJECT_TRACK_ISSUE:READ+CREATE')" @click="syncIssues"/>
<ms-table-button icon="el-icon-upload2" :content="$t('commons.import')" v-if="hasPermission('PROJECT_TRACK_ISSUE:READ+CREATE')" @click="handleImport"/>
<ms-table-button icon="el-icon-download" :content="$t('commons.export')" v-if="hasPermission('PROJECT_TRACK_ISSUE:READ')" @click="handleExport"/>
</template>
</ms-table-header>
</template>
<ms-table
v-loading="page.result.loading || loading"
row-key="id"
:data="page.data"
:enableSelection="false"
:condition="page.condition"
:total="page.total"
:page-size.sync="page.pageSize"
:operators="operators"
:show-select-all="false"
:batch-operators="batchButtons"
:screen-height="screenHeight"
:remember-order="true"
:fields.sync="fields"
@ -33,139 +31,141 @@
@filter="search"
@order="getIssues"
@handlePageChange="getIssues"
ref="table"
>
<span v-for="(item) in fields" :key="item.key">
<ms-table-column width="1">
</ms-table-column>
<ms-table-column
:label="$t('test_track.issue.id')"
prop="num"
:field="item"
sortable
min-width="100"
:fields-width="fieldsWidth">
</ms-table-column>
ref="table">
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
:label="$t('test_track.issue.title')"
sortable
min-width="110"
prop="title">
</ms-table-column>
<span v-for="(item) in fields" :key="item.key">
<ms-table-column width="1">
</ms-table-column>
<ms-table-column
:label="$t('test_track.issue.id')"
prop="num"
:field="item"
sortable
min-width="100"
:fields-width="fieldsWidth">
</ms-table-column>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
:filters="platformFilters"
:label="$t('test_track.issue.platform')"
min-width="80"
prop="platform">
</ms-table-column>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
:label="$t('test_track.issue.title')"
sortable
min-width="110"
prop="title">
</ms-table-column>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
sortable
min-width="110"
:label="$t('test_track.issue.platform_status') "
prop="platformStatus">
<template v-slot="scope">
<span v-if="scope.row.platform ==='Zentao'">{{ scope.row.platformStatus ? issueStatusMap[scope.row.platformStatus] : '--'}}</span>
<span v-else-if="scope.row.platform ==='Tapd'">{{ scope.row.platformStatus ? tapdIssueStatusMap[scope.row.platformStatus] : '--'}}</span>
<span v-else>{{ scope.row.platformStatus ? scope.row.platformStatus : '--'}}</span>
</template>
</ms-table-column>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
:filters="platformFilters"
:label="$t('test_track.issue.platform')"
min-width="80"
prop="platform">
</ms-table-column>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
column-key="creator"
:filters="creatorFilters"
sortable
min-width="100px"
:label="$t('custom_field.issue_creator')"
prop="creatorName">
</ms-table-column>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
sortable
min-width="110"
:label="$t('test_track.issue.platform_status') "
prop="platformStatus">
<template v-slot="scope">
<span v-if="scope.row.platform ==='Zentao'">{{ scope.row.platformStatus ? issueStatusMap[scope.row.platformStatus] : '--'}}</span>
<span v-else-if="scope.row.platform ==='Tapd'">{{ scope.row.platformStatus ? tapdIssueStatusMap[scope.row.platformStatus] : '--'}}</span>
<span v-else>{{ scope.row.platformStatus ? scope.row.platformStatus : '--'}}</span>
</template>
</ms-table-column>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
:label="$t('test_track.issue.issue_resource')"
prop="resourceName">
<template v-slot="scope">
<el-link v-if="scope.row.resourceName" @click="$router.push('/track/plan/view/' + scope.row.resourceId)">
{{ scope.row.resourceName }}
</el-link>
<span v-else>
--
</span>
</template>
</ms-table-column>
<ms-table-column prop="createTime"
:field="item"
:fields-width="fieldsWidth"
:label="$t('commons.create_time')"
sortable
min-width="180px">
<template v-slot:default="scope">
<span>{{ scope.row.createTime | datetimeFormat }}</span>
</template>
</ms-table-column >
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
column-key="creator"
:filters="creatorFilters"
sortable
min-width="100px"
:label="$t('custom_field.issue_creator')"
prop="creatorName">
</ms-table-column>
<issue-description-table-item :fields-width="fieldsWidth" :field="item"/>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
:label="$t('test_track.issue.issue_resource')"
prop="resourceName">
<template v-slot="scope">
<el-link v-if="scope.row.resourceName" @click="$router.push('/track/plan/view/' + scope.row.resourceId)">
{{ scope.row.resourceName }}
</el-link>
<span v-else>
--
</span>
</template>
</ms-table-column>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
:label="item.label"
prop="caseCount">
<template v-slot="scope">
<router-link :to="scope.row.caseCount > 0 ? {name: 'testCase', params: { projectId: 'all', ids: scope.row.caseIds }} : {}">
{{scope.row.caseCount}}
</router-link>
</template>
</ms-table-column>
<ms-table-column v-for="field in issueTemplate.customFields" :key="field.id"
:filters="field.name === '状态'? i18nCustomStatus(getCustomFieldFilter(field)) : getCustomFieldFilter(field)"
sortable="custom"
<ms-table-column prop="createTime"
:field="item"
:fields-width="fieldsWidth"
min-width="120"
:label="field.system ? $t(systemNameMap[field.name]) :field.name"
:column-key="generateColumnKey(field)"
:prop="field.name">
<template v-slot="scope">
<span v-if="field.name === '状态'">
{{getCustomFieldValue(scope.row, field, issueStatusMap[scope.row.status])}}
</span>
<span v-else-if="field.type === 'richText'">
<el-popover
placement="right"
width="500"
trigger="hover"
popper-class="issues-popover">
<ms-mark-down-text prop="value" :data="{value: getCustomFieldValue(scope.row, field)}" :disabled="true"/>
<el-button slot="reference" type="text">{{ $t('test_track.issue.preview') }}</el-button>
</el-popover>
</span>
<span v-else>
{{getCustomFieldValue(scope.row, field)}}
</span>
:label="$t('commons.create_time')"
sortable
min-width="180px">
<template v-slot:default="scope">
<span>{{ scope.row.createTime | datetimeFormat }}</span>
</template>
</ms-table-column>
</ms-table-column >
</span>
<issue-description-table-item :fields-width="fieldsWidth" :field="item"/>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
:label="item.label"
prop="caseCount">
<template v-slot="scope">
<router-link :to="scope.row.caseCount > 0 ? {name: 'testCase', params: { projectId: 'all', ids: scope.row.caseIds }} : {}">
{{scope.row.caseCount}}
</router-link>
</template>
</ms-table-column>
<ms-table-column v-for="field in issueTemplate.customFields" :key="field.id"
:filters="field.name === '状态'? i18nCustomStatus(getCustomFieldFilter(field)) : getCustomFieldFilter(field)"
sortable="custom"
:field="item"
:fields-width="fieldsWidth"
min-width="200"
:label="field.system ? $t(systemNameMap[field.name]) :field.name"
:column-key="generateColumnKey(field)"
:prop="field.name">
<template v-slot="scope">
<span v-if="field.name === '状态'">
{{getCustomFieldValue(scope.row, field, issueStatusMap[scope.row.status])}}
</span>
<span v-else-if="field.type === 'richText'">
<el-popover
placement="right"
width="500"
trigger="hover"
popper-class="issues-popover">
<ms-mark-down-text prop="value" :data="{value: getCustomFieldValue(scope.row, field)}" :disabled="true"/>
<el-button slot="reference" type="text">{{ $t('test_track.issue.preview') }}</el-button>
</el-popover>
</span>
<span v-else>
{{getCustomFieldValue(scope.row, field)}}
</span>
</template>
</ms-table-column>
</span>
</ms-table>
<ms-table-pagination :change="getIssues" :current-page.sync="page.currentPage" :page-size.sync="page.pageSize"
:total="page.total"/>
<issue-edit @refresh="getIssues" ref="issueEdit"/>
<issue-sync-select @syncConfirm="syncConfirm" ref="issueSyncSelect" />
<issue-import @refresh="getIssues" ref="issueImport"/>
<issue-export @export="exportIssue" ref="issueExport"/>
</el-card>
</ms-main-container>
</ms-container>
@ -187,13 +187,15 @@ import MsTableHeader from "metersphere-frontend/src/components/MsTableHeader";
import IssueDescriptionTableItem from "@/business/issue/IssueDescriptionTableItem";
import IssueEdit from "@/business/issue/IssueEdit";
import IssueSyncSelect from "@/business/issue/IssueSyncSelect";
import IssueImport from "@/business/issue/components/import/IssueImport";
import IssueExport from "@/business/issue/components/export/IssueExport";
import {
checkSyncIssues,
getIssuePartTemplateWithProject,
getIssues,
syncIssues,
deleteIssue,
getIssuesById
getIssuesById, batchDeleteIssue
} from "@/api/issue";
import {
getCustomFieldValue,
@ -202,7 +204,8 @@ import {
} from "metersphere-frontend/src/utils/tableUtils";
import MsContainer from "metersphere-frontend/src/components/MsContainer";
import MsMainContainer from "metersphere-frontend/src/components/MsMainContainer";
import {getCurrentProjectID, getCurrentWorkspaceId} from "metersphere-frontend/src/utils/token";
import {getCurrentProjectID, getCurrentWorkspaceId, getCurrentUserId} from "metersphere-frontend/src/utils/token";
import {hasPermission} from "metersphere-frontend/src/utils/permission";
import {getProjectMember, getProjectMemberUserFilter} from "@/api/user";
import {LOCAL} from "metersphere-frontend/src/utils/constants";
import {TEST_TRACK_ISSUE_LIST} from "metersphere-frontend/src/components/search/search-components";
@ -221,6 +224,8 @@ export default {
IssueEdit,
IssueDescriptionTableItem,
IssueSyncSelect,
IssueImport,
IssueExport,
MsTableHeader,
MsTablePagination, MsTableButton, MsTableOperators, MsTableColumn, MsTable
},
@ -250,6 +255,13 @@ export default {
permissions: ['PROJECT_TRACK_ISSUE:READ+DELETE']
}
],
batchButtons: [
{
name: this.$t('test_track.issue.batch_delete_issue'),
handleClick: this.handleBatchDelete,
permissions: ['PROJECT_TRACK_ISSUE:READ+DELETE']
}
],
issueTemplate: {},
members: [],
userFilter: [],
@ -308,7 +320,11 @@ export default {
this.editParam();
},
methods: {
generateColumnKey,
generateColumnKey(field){
let columnKey = generateColumnKey(field);
return "custom_" + columnKey.substr(columnKey.indexOf("-") + 1);
},
hasPermission,
tableDoLayout() {
if (this.$refs.table) this.$refs.table.doLayout();
},
@ -404,6 +420,28 @@ export default {
this.getIssues();
})
},
handleBatchDelete() {
this.$alert(this.$t('test_track.issue.batch_delete_tip') + " ", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
this._handleBatchDelete();
}
}
});
},
_handleBatchDelete() {
let selectIds = this.$refs.table.selectIds;
if (selectIds.length == 0) {
this.$warning(this.$t("test_track.issue.check_select"));
return;
}
batchDeleteIssue({"batchDeleteIds" : selectIds, "batchDeleteAll" : this.page.condition.selectAll})
.then(() => {
this.$success(this.$t('commons.delete_success'));
this.getIssues();
})
},
btnDisable(row) {
if (this.issueTemplate.platform !== row.platform) {
return true;
@ -413,6 +451,29 @@ export default {
syncIssues() {
this.$refs.issueSyncSelect.open();
},
handleImport() {
this.$refs.issueImport.open();
},
handleExport() {
let exportIds = this.$refs.table.selectIds;
if (exportIds.length == 0) {
this.$warning(this.$t("test_track.issue.check_select"));
return;
}
this.$refs.issueExport.open();
},
exportIssue(data) {
let param = {
"projectId": getCurrentProjectID(),
"workspaceId": getCurrentWorkspaceId(),
"userId": getCurrentUserId(),
"isSelectAll": this.page.condition.selectAll,
"exportIds": this.$refs.table.selectIds,
"exportFields": data,
"orders": getLastTableSortField(this.tableHeaderKey)
}
this.$fileDownloadPost("/issues/export", param);
},
syncConfirm(data) {
this.loading = true;
let param = {
@ -453,4 +514,8 @@ export default {
.el-table {
cursor: pointer;
}
:deep(.el-table) {
overflow: auto;
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<el-dialog class="issue-export"
v-loading="loading"
:title="$t('test_track.issue.export')"
:visible.sync="dialogVisible"
@close="close">
<issue-export-field-select ref="issueExportFieldSelect"/>
<span slot="footer" class="dialog-footer">
<el-button size="mini" @click="dialogVisible = false">{{ $t('commons.cancel') }}</el-button>
<el-button type="primary" size="mini" @click="exportIssue">{{ $t('commons.export') }}</el-button>
</span>
</el-dialog>
</template>
<script>
import ElUploadList from "element-ui/packages/upload/src/upload-list";
import MsTableButton from 'metersphere-frontend/src/components/MsTableButton';
import {listenGoBack, removeGoBackListener} from "metersphere-frontend/src/utils";
import {getCurrentProjectID} from "metersphere-frontend/src/utils/token"
import IssueExportFieldSelect from "@/business/issue/components/export/IssueExportFieldSelect";
export default {
name: "IssueExport",
components: {IssueExportFieldSelect, ElUploadList, MsTableButton},
data() {
return {
dialogVisible: false,
projectId: "",
loading: false
}
},
activated() {
},
methods: {
open() {
listenGoBack(this.close);
this.projectId = getCurrentProjectID();
this.dialogVisible = true;
this.loading = false;
},
close() {
removeGoBackListener(this.close);
this.dialogVisible = false;
this.loading = false;
},
exportIssue() {
let param = null;
if (this.$refs.issueExportFieldSelect) {
param = this.$refs.issueExportFieldSelect.getExportParam();
}
this.close();
this.$emit('export', param);
}
}
}
</script>
<style>
</style>
<style scoped>
.issue-export :deep(.el-dialog) {
width: 600px;
}
.issue-export :deep(.el-dialog .el-dialog__title) {
font-weight: bold;
}
.issue-export :deep(.el-dialog .el-dialog__body) {
padding: 20px;
}
</style>

View File

@ -0,0 +1,49 @@
<template>
<div>
<el-row v-for="rowIndex in fieldRowCount" :key="rowIndex">
<span v-for="(item, index) in fields"
:key="item.id">
<el-col :span="6"
v-if="Math.floor(index / colCountEachRow) === rowIndex - 1">
<el-checkbox
v-model="item.enable"
:disabled="item.disabled"
@change="change">
{{ item.name }}
</el-checkbox>
</el-col>
</span>
</el-row>
</div>
</template>
<script>
export default {
name: "IssueExportFieldList",
props: ['fields'],
data() {
return {
colCountEachRow: 4
}
},
computed: {
fieldRowCount() {
if (!this.fields) {
return 0;
}
return Math.ceil(this.fields.length / this.colCountEachRow);
}
},
methods: {
change(value) {
this.$emit('enableChange', value);
}
}
}
</script>
<style scoped>
.el-row {
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,192 @@
<template>
<div v-loading="loading">
<div class="export-title"
@click="showSelect = !showSelect">
<span>
{{ $t('test_track.case.import.select_import_field') }}
</span>
<i class="el-icon-arrow-down"
v-if="showSelect"/>
<i class="el-icon-arrow-left"
v-if="!showSelect"/>
</div>
<el-divider/>
<div v-show="showSelect">
<el-checkbox class="select-all-checkbox"
v-model="selectAll" @change="handleSelectAllChange">
{{ $t('test_track.case.import.select_import_all_field') }}
</el-checkbox>
<issue-export-field-select-item
type="EXPORT_BASE_FIELD"
:title="$t('test_track.case.import.base_field')"
:fields="baseFields"
@selectAllChange="handleItemSelectAllChange"
ref="baseSelectItem"/>
<issue-export-field-select-item
type="EXPORT_CUSTOM_FIELD"
:title="$t('test_track.case.import.custom_field')"
:fields="customFields"
@selectAllChange="handleItemSelectAllChange"
ref="customSelectItem"/>
<issue-export-field-select-item
type="EXPORT_OTHER_FIELD"
:title="$t('test_track.case.import.other_field')"
:fields="otherFields"
@selectAllChange="handleItemSelectAllChange"
ref="otherSelectItem"/>
<div class="other-field-tip">
{{ $t('test_track.case.import.other_field_tip') }}
</div>
</div>
</div>
</template>
<script>
import IssueExportFieldSelectItem from "@/business/issue/components/export/IssueExportFieldSelectItem";
import {getIssuePartTemplateWithProject} from "@/api/issue";
export default {
name: "IssueExportFieldSelect",
components: {IssueExportFieldSelectItem},
data() {
return {
selectAll: false,
showSelect: true,
baseFields: [
{
id: 'id',
key: 'A',
name: 'ID',
enable: true,
disabled: true
},
{
id: 'title',
key: 'B',
name: this.$t("test_track.issue.title"),
enable: true,
disabled: true
},
{
id: 'description',
key: 'D',
name: this.$t("test_track.issue.description"),
enable: true
}
],
loading: false,
customFields: [],
otherFields: [
{
id: 'creator',
key: 'E',
name: this.$t("commons.creator"),
enable: true
},
{
id: 'caseCount',
key: 'A',
name: this.$t("test_track.home.case_size"),
enable: true
},
{
id: 'comment',
key: 'B',
name: this.$t("commons.comment"),
enable: true
},
{
id: 'resource',
key: 'C',
name: this.$t("test_track.issue.issue_resource"),
enable: true
},
{
id: 'platform',
key: 'D',
name: this.$t("test_track.issue.issue_platform"),
enable: true
},
{
id: 'createTime',
key: 'E',
name: this.$t("commons.create_time"),
enable: true
},
]
}
},
computed: {
selectItems() {
return [this.$refs.baseSelectItem, this.$refs.customSelectItem, this.$refs.otherSelectItem];
}
},
created() {
this.loading = true;
getIssuePartTemplateWithProject((template) => {
template.customFields.forEach(item => {
item.enable = true;
this.customFields.push(item);
});
this.loading = false;
});
},
methods: {
getExportParam() {
return {
baseHeaders: this.selectItems[0].getExportParam(),
customHeaders: this.selectItems[1].getExportParam(),
otherHeaders: this.selectItems[2].getExportParam(),
}
},
handleSelectAllChange() {
this.selectItems.forEach(item => {
item.selectAllChange(this.selectAll);
});
},
handleItemSelectAllChange() {
let isSelectAll = true;
this.selectItems.forEach(item => {
if (!item.selectAll) {
isSelectAll = false;
}
});
this.selectAll = isSelectAll;
}
}
}
</script>
<style scoped>
.export-title {
font-size: 16px;
font-weight: bold;
margin: 20px 5px 15px 0px;
}
.export-title span:first-child {
margin-right: 5px;
}
.select-all-checkbox {
margin-top: 10px
}
.export-title {
cursor: pointer;
}
.other-field-tip {
margin-top: 30px;
font-size: 10px;
color: #9ea0a3;
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<div>
<div class="field-title" v-if="fields && fields.length > 0">
<span>{{ title }}</span>
<el-checkbox
v-model="selectAll"
@change="selectAllChange"/>
</div>
<issue-export-field-list
:fields="fields"
@enableChange="enableChange"
/>
</div>
</template>
<script>
import IssueExportFieldList from "@/business/issue/components/export/IssueExportFieldList";
export default {
name: "IssueExportFieldSelectItem",
components: {IssueExportFieldList},
props: {
fields: Array,
title: String,
type: String
},
data() {
return {
selectAll: false,
}
},
watch: {
selectAll() {
this.$emit('selectAllChange', this.selectAll);
},
fields() {
this.checkEnable();
}
},
created() {
this.checkEnable();
},
methods: {
getExportParam() {
return this.fields.filter(item => item.enable);
},
enableChange(enable) {
this.persistenceValues();
if (enable) {
for (let head of this.fields) {
if (!head.enable) {
//
return;
}
}
}
//
this.selectAll = enable;
},
persistenceValues() {
//
let enableKeys = this.fields.filter(i => i.enable)
.map(i => i.key);
localStorage.setItem(this.type, JSON.stringify(enableKeys));
},
selectAllChange(value) {
this.selectAll = value;
this.fields.forEach(i => {
if (!i.disabled) {
i.enable = value;
}
});
this.persistenceValues();
},
checkEnable() {
//
let enableKeys = localStorage.getItem(this.type);
if (enableKeys) {
enableKeys = JSON.parse(enableKeys);
}
let isSelectAll = true;
for (let field of this.fields) {
if (enableKeys) {
if (enableKeys.indexOf(field.key) > -1) {
field.enable = true;
} else {
field.enable = false;
}
}
if (!field.enable) {
isSelectAll = false;
}
}
this.selectAll = isSelectAll;
}
}
}
</script>
<style scoped>
.field-title {
margin-top: 20px;
margin-bottom: 10px;
font-size: 15px;
font-weight: bold;
}
.field-title span:first-child {
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,156 @@
<template>
<el-dialog :visible="visible" v-loading="loading" :title="$t('test_track.issue.import_bugs')" @close="cancel" width="35%">
<div>
<el-row>
<span style="color: red">*</span> {{ $t('test_track.issue.import_type') }}
<el-select v-model="importType" :placeholder="$t('commons.please_select')" size="mini" class="issue-import-type" clearable>
<el-option
v-for="item in importOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
<el-tooltip effect="dark" style="margin-left: 10px" placement="right">
<div slot="content" v-html="$t('test_track.issue.import_type_tips')"></div>
<i class="el-icon-info"></i>
</el-tooltip>
</el-row>
<el-row>
<el-upload
class="issue-upload" drag action="alert"
:limit="1"
:file-list="uploadFiles"
:http-request="handleUpload"
:on-remove="handleRemove" accept=".xls, .xlsx">
<i class="el-icon-upload"></i>
<div class="el-upload__text" v-html="$t('load_test.upload_tips')"></div>
<div class="el-upload__tip" slot="tip">
{{ $t('test_track.issue.import_file_limit_tips') }}
<el-link type="primary" class="download-template" @click="downloadIssueImportTemplate">
{{ $t('test_track.case.import.download_template') }}
</el-link>
</div>
</el-upload>
</el-row>
<el-row>
<ul>
<li v-for="errFile in errList" :key="errFile.rowNum">
{{ errFile.errMsg }}
</li>
</ul>
</el-row>
<el-row style="text-align: right; margin-top: 40px">
<el-button size="mini" @click="cancel">{{ $t('commons.cancel') }}</el-button>
<el-button type="primary" size="mini" @click="save">
{{ $t('commons.save') }}
</el-button>
</el-row>
</div>
</el-dialog>
</template>
<script>
import {getCurrentProjectID, getCurrentUserId, getCurrentWorkspaceId} from "metersphere-frontend/src/utils/token";
export default {
name: "IssueImport",
props: ['tabName', 'name'],
data() {
return {
visible:false,
loading: false,
importType: "",
importOptions: [{value: "Update", label: this.$t('commons.cover')}, {value: "Create", label: this.$t('commons.not_cover')}],
uploadFiles: [],
errList: [],
}
},
created() {
},
computed: {
projectId() {
return getCurrentProjectID();
}
},
methods: {
open() {
this.visible = true;
},
cancel() {
this.visible = false;
this.importType = "";
this.uploadFiles = [];
},
handleUpload(file) {
this.uploadFiles.push(file.file);
},
handleRemove(file) {
let fileName = file.name ? file.name : file.file.name
for (let i = 0; i < this.uploadFiles.length; i++) {
let uploadFileName = this.uploadFiles[i].name ? this.uploadFiles[i].name : this.uploadFiles[i].file.name;
if (fileName === uploadFileName) {
this.uploadFiles.splice(i, 1);
break;
}
}
},
downloadIssueImportTemplate() {
let uri = '/issues/import/template/download/';
this.$fileDownload(uri + getCurrentProjectID());
},
save() {
let param = {
workspaceId: getCurrentWorkspaceId(),
projectId: getCurrentProjectID(),
userId: getCurrentUserId(),
importType: this.importType
};
if (this.importType == '') {
this.$warning(this.$t('test_track.case.import.import_type_require_tips'))
return;
}
if (this.uploadFiles.length == 0) {
this.$warning(this.$t('test_track.case.import.import_file_tips'));
return;
}
this.loading = true;
this.$fileUpload('/issues/import', this.uploadFiles[0], param)
.then(response => {
this.loading = false;
let res = response.data;
if (res.success) {
this.$success(this.$t('test_track.case.import.success'));
this.cancel();
this.$emit("refresh");
} else {
this.errList = res.errList;
}
}).catch((err) => {
this.loading = false;
});
}
}
}
</script>
<style scoped>
.issue-import-type {
margin-left: 6px;
}
.issue-upload {
margin-left: 75px;
margin-top: 20px;
}
.download-template {
margin-left: 220px;
top: -15px;
font-size: 5px;
font-weight: 600;
}
</style>