feat(测试跟踪): 用例及缺陷附件支持文件管理

This commit is contained in:
song-cc-rock 2022-09-17 13:21:28 +08:00 committed by 刘瑞斌
parent d359bbf66f
commit c0870f06e5
32 changed files with 924 additions and 97 deletions

View File

@ -27,6 +27,7 @@
<xmlbeans.version>5.1.0</xmlbeans.version>
<poi.version>5.1.0</poi.version>
<jgit.version>6.2.0.202206071550-r</jgit.version>
<commons-fileupload.version>1.3</commons-fileupload.version>
</properties>
<dependencies>
@ -152,6 +153,17 @@
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
<exclusions>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>

View File

@ -12,5 +12,7 @@ public class AttachmentModuleRelation implements Serializable {
private String attachmentId;
private String fileMetadataRefId;
private static final long serialVersionUID = 1L;
}

View File

@ -313,6 +313,76 @@ public class AttachmentModuleRelationExample {
addCriterion("attachment_id not between", value1, value2, "attachmentId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdIsNull() {
addCriterion("file_metadata_ref_id is null");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdIsNotNull() {
addCriterion("file_metadata_ref_id is not null");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdEqualTo(String value) {
addCriterion("file_metadata_ref_id =", value, "fileMetadataRefId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdNotEqualTo(String value) {
addCriterion("file_metadata_ref_id <>", value, "fileMetadataRefId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdGreaterThan(String value) {
addCriterion("file_metadata_ref_id >", value, "fileMetadataRefId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdGreaterThanOrEqualTo(String value) {
addCriterion("file_metadata_ref_id >=", value, "fileMetadataRefId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdLessThan(String value) {
addCriterion("file_metadata_ref_id <", value, "fileMetadataRefId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdLessThanOrEqualTo(String value) {
addCriterion("file_metadata_ref_id <=", value, "fileMetadataRefId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdLike(String value) {
addCriterion("file_metadata_ref_id like", value, "fileMetadataRefId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdNotLike(String value) {
addCriterion("file_metadata_ref_id not like", value, "fileMetadataRefId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdIn(List<String> values) {
addCriterion("file_metadata_ref_id in", values, "fileMetadataRefId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdNotIn(List<String> values) {
addCriterion("file_metadata_ref_id not in", values, "fileMetadataRefId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdBetween(String value1, String value2) {
addCriterion("file_metadata_ref_id between", value1, value2, "fileMetadataRefId");
return (Criteria) this;
}
public Criteria andFileMetadataRefIdNotBetween(String value1, String value2) {
addCriterion("file_metadata_ref_id not between", value1, value2, "fileMetadataRefId");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {

View File

@ -1,8 +1,9 @@
package io.metersphere.base.domain;
import java.io.Serializable;
import lombok.Data;
import java.io.Serializable;
@Data
public class FileAttachmentMetadata implements Serializable {
private String id;
@ -21,5 +22,9 @@ public class FileAttachmentMetadata implements Serializable {
private String filePath;
private Boolean isLocal;
private Boolean isRelatedDeleted;
private static final long serialVersionUID = 1L;
}

View File

@ -5,6 +5,7 @@
<result column="relation_id" jdbcType="VARCHAR" property="relationId" />
<result column="relation_type" jdbcType="VARCHAR" property="relationType" />
<result column="attachment_id" jdbcType="VARCHAR" property="attachmentId" />
<result column="file_metadata_ref_id" jdbcType="VARCHAR" property="fileMetadataRefId" />
</resultMap>
<sql id="Example_Where_Clause">
<where>
@ -65,7 +66,7 @@
</where>
</sql>
<sql id="Base_Column_List">
relation_id, relation_type, attachment_id
relation_id, relation_type, attachment_id, file_metadata_ref_id
</sql>
<select id="selectByExample" parameterType="io.metersphere.base.domain.AttachmentModuleRelationExample" resultMap="BaseResultMap">
select
@ -88,10 +89,10 @@
</if>
</delete>
<insert id="insert" parameterType="io.metersphere.base.domain.AttachmentModuleRelation">
insert into attachment_module_relation (relation_id, relation_type, attachment_id
)
values (#{relationId,jdbcType=VARCHAR}, #{relationType,jdbcType=VARCHAR}, #{attachmentId,jdbcType=VARCHAR}
)
insert into attachment_module_relation (relation_id, relation_type, attachment_id,
file_metadata_ref_id)
values (#{relationId,jdbcType=VARCHAR}, #{relationType,jdbcType=VARCHAR}, #{attachmentId,jdbcType=VARCHAR},
#{fileMetadataRefId,jdbcType=VARCHAR})
</insert>
<insert id="insertSelective" parameterType="io.metersphere.base.domain.AttachmentModuleRelation">
insert into attachment_module_relation
@ -105,6 +106,9 @@
<if test="attachmentId != null">
attachment_id,
</if>
<if test="fileMetadataRefId != null">
file_metadata_ref_id,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="relationId != null">
@ -116,6 +120,9 @@
<if test="attachmentId != null">
#{attachmentId,jdbcType=VARCHAR},
</if>
<if test="fileMetadataRefId != null">
#{fileMetadataRefId,jdbcType=VARCHAR},
</if>
</trim>
</insert>
<select id="countByExample" parameterType="io.metersphere.base.domain.AttachmentModuleRelationExample" resultType="java.lang.Long">
@ -136,6 +143,9 @@
<if test="record.attachmentId != null">
attachment_id = #{record.attachmentId,jdbcType=VARCHAR},
</if>
<if test="record.fileMetadataRefId != null">
file_metadata_ref_id = #{record.fileMetadataRefId,jdbcType=VARCHAR},
</if>
</set>
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
@ -145,7 +155,8 @@
update attachment_module_relation
set relation_id = #{record.relationId,jdbcType=VARCHAR},
relation_type = #{record.relationType,jdbcType=VARCHAR},
attachment_id = #{record.attachmentId,jdbcType=VARCHAR}
attachment_id = #{record.attachmentId,jdbcType=VARCHAR},
file_metadata_ref_id = #{record.fileMetadataRefId,jdbcType=VARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>

View File

@ -3,10 +3,10 @@
<mapper namespace="io.metersphere.base.mapper.ext.ExtAttachmentModuleRelationMapper">
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO
attachment_module_relation (relation_id, relation_type, attachment_id)
attachment_module_relation (relation_id, relation_type, attachment_id, file_metadata_ref_id)
VALUES
<foreach collection="attachmentModuleRelations" item="relation" separator="," >
(#{relation.relationId}, #{relation.relationType}, #{relation.attachmentId})
(#{relation.relationId}, #{relation.relationType}, #{relation.attachmentId}, #{relation.fileMetadataRefId})
</foreach>
</insert>
</mapper>

View File

@ -1,5 +1,5 @@
package io.metersphere.commons.constants;
public enum FileAssociationType {
API, CASE, SCENARIO, UI, ENVIRONMENT
API, CASE, SCENARIO, UI, ENVIRONMENT, TEST_CASE, ISSUE
}

View File

@ -0,0 +1,8 @@
package io.metersphere.metadata.vo;
import lombok.Data;
@Data
public class AttachmentDumpRequest extends DumpFileRequest{
private String attachmentId;
}

View File

@ -8,6 +8,7 @@ import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.*;
import io.metersphere.performance.request.QueryProjectFileRequest;
import org.apache.commons.collections.CollectionUtils;
import io.metersphere.xmind.utils.FileUtil;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@ -50,6 +51,12 @@ public class FileService {
return FileUtils.fileToByte(attachmentFile);
}
public MultipartFile getAttachmentMultipartFile(String id) {
FileAttachmentMetadata fileAttachmentMetadata = fileAttachmentMetadataMapper.selectByPrimaryKey(id);
File attachmentFile = new File(fileAttachmentMetadata.getFilePath() + "/" + fileAttachmentMetadata.getName());
return FileUtil.fileToMultipartFile(attachmentFile);
}
public FileContent getFileContent(String fileId) {
return fileContentMapper.selectByPrimaryKey(fileId);
}

View File

@ -1,6 +1,8 @@
package io.metersphere.track.controller;
import io.metersphere.base.domain.FileAttachmentMetadata;
import io.metersphere.metadata.service.FileMetadataService;
import io.metersphere.metadata.vo.AttachmentDumpRequest;
import io.metersphere.service.FileService;
import io.metersphere.track.request.attachment.AttachmentRequest;
import io.metersphere.track.request.testplan.FileOperationRequest;
@ -14,6 +16,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
@ -27,15 +30,23 @@ public class AttachmentController {
private FileService fileService;
@Resource
private AttachmentService attachmentService;
@Resource
private FileMetadataService fileMetadataService;
@PostMapping(value = "/upload", consumes = {"multipart/form-data"})
public void uploadAttachment(@RequestPart("request") AttachmentRequest request, @RequestPart(value = "file", required = false) MultipartFile file) {
attachmentService.uploadAttachment(request, file);
}
@GetMapping("/preview/{fileId}")
public ResponseEntity<byte[]> previewAttachment(@PathVariable String fileId) {
byte[] bytes = fileService.getAttachmentBytes(fileId);
@GetMapping("/preview/{fileId}/{isLocal}")
public ResponseEntity<byte[]> previewAttachment(@PathVariable String fileId, @PathVariable Boolean isLocal) {
byte[] bytes;
if (isLocal) {
bytes = fileService.getAttachmentBytes(fileId);
} else {
String refId = attachmentService.getRefIdByAttachmentId(fileId);
bytes = fileMetadataService.loadFileAsBytes(refId);
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileId + "\"")
@ -44,7 +55,13 @@ public class AttachmentController {
@PostMapping("/download")
public ResponseEntity<byte[]> downloadAttachment(@RequestBody FileOperationRequest fileOperationRequest) {
byte[] bytes = fileService.getAttachmentBytes(fileOperationRequest.getId());
byte[] bytes;
if (fileOperationRequest.getIsLocal()) {
bytes = fileService.getAttachmentBytes(fileOperationRequest.getId());
} else {
String refId = attachmentService.getRefIdByAttachmentId(fileOperationRequest.getId());
bytes = fileMetadataService.loadFileAsBytes(refId);
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + URLEncoder.encode(fileOperationRequest.getName(), StandardCharsets.UTF_8) + "\"")
@ -61,4 +78,22 @@ public class AttachmentController {
public List<FileAttachmentMetadata> listMetadata(@RequestBody AttachmentRequest request) {
return attachmentService.listMetadata(request);
}
@PostMapping("/metadata/relate")
public void relate(@RequestBody AttachmentRequest request) {
attachmentService.relate(request);
}
@PostMapping("/metadata/unrelated")
public void unrelated(@RequestBody AttachmentRequest request) {
attachmentService.unrelated(request);
}
@PostMapping(value = "/metadata/dump")
public void dumpFile(@RequestBody AttachmentDumpRequest request) {
List<MultipartFile> files = new ArrayList<>();
MultipartFile file = fileService.getAttachmentMultipartFile(request.getAttachmentId());
files.add(file);
fileMetadataService.dumpFile(request, files);
}
}

View File

@ -1,6 +1,5 @@
package io.metersphere.track.issue;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.base.domain.*;

View File

@ -2,6 +2,8 @@ package io.metersphere.track.request.attachment;
import lombok.Data;
import java.util.List;
/**
* @author songcc
@ -14,4 +16,6 @@ public class AttachmentRequest {
private String belongId;
private String copyBelongId;
private List<String> metadataRefIds;
}

View File

@ -28,6 +28,10 @@ public class EditTestCaseRequest extends TestCaseWithBLOBs {
private String copyCaseId;
// 是否处理附件文件
private boolean handleAttachment = true;
// 关联文件管理引用ID
private List<String> relateFileMetaIds = new ArrayList<>();
// 取消关联文件应用ID
private List<String> unRelateFileMetaIds = new ArrayList<>();
/**
* 创建新版本时 是否连带复制其他信息的配置类

View File

@ -8,6 +8,7 @@ import io.metersphere.track.dto.PlatformStatusDTO;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Getter
@ -53,4 +54,8 @@ public class IssuesUpdateRequest extends IssuesWithBLOBs {
* 复制缺陷时原始缺陷ID
*/
private String copyIssueId;
// 关联文件管理引用ID
private List<String> relateFileMetaIds = new ArrayList<>();
// 取消关联文件应用ID
private List<String> unRelateFileMetaIds = new ArrayList<>();
}

View File

@ -8,4 +8,5 @@ import lombok.Setter;
public class FileOperationRequest {
private String id;
private String name;
private Boolean isLocal;
}

View File

@ -5,24 +5,31 @@ import io.metersphere.base.mapper.*;
import io.metersphere.base.mapper.ext.ExtAttachmentModuleRelationMapper;
import io.metersphere.commons.constants.AttachmentSyncType;
import io.metersphere.commons.constants.AttachmentType;
import io.metersphere.commons.constants.FileAssociationType;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.commons.utils.FileUtils;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.i18n.Translator;
import io.metersphere.metadata.service.FileMetadataService;
import io.metersphere.service.FileService;
import io.metersphere.track.issue.IssueFactory;
import io.metersphere.track.request.attachment.AttachmentRequest;
import io.metersphere.track.request.testcase.IssuesRequest;
import io.metersphere.track.request.testcase.IssuesUpdateRequest;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
/**
@ -48,6 +55,14 @@ public class AttachmentService {
private AttachmentModuleRelationMapper attachmentModuleRelationMapper;
@Resource
private ExtAttachmentModuleRelationMapper extAttachmentModuleRelationMapper;
@Resource
private FileMetadataMapper fileMetadataMapper;
@Resource
private FileAssociationMapper fileAssociationMapper;
@Resource
private FileMetadataService fileMetadataService;
@Resource
SqlSessionFactory sqlSessionFactory;
public void uploadAttachment(AttachmentRequest request, MultipartFile file) {
// 附件上传的前置校验
@ -129,25 +144,162 @@ public class AttachmentService {
example.createCriteria().andRelationIdEqualTo(request.getCopyBelongId()).andRelationTypeEqualTo(request.getBelongType());
List<AttachmentModuleRelation> attachmentModuleRelations = attachmentModuleRelationMapper.selectByExample(example);
if (CollectionUtils.isNotEmpty(attachmentModuleRelations)) {
attachmentModuleRelations.forEach(attachmentModuleRelation -> {
FileAttachmentMetadata fileAttachmentMetadata = fileService.copyAttachment(attachmentModuleRelation.getAttachmentId(), request.getBelongType(), request.getBelongId());
// 本地附件
List<String> localAttachments = attachmentModuleRelations.stream()
.filter(relation -> StringUtils.isEmpty(relation.getFileMetadataRefId()))
.map(AttachmentModuleRelation::getAttachmentId)
.filter(StringUtils::isNotEmpty).collect(Collectors.toList());
localAttachments.forEach(localAttachmentId -> {
FileAttachmentMetadata fileAttachmentMetadata = fileService.copyAttachment(localAttachmentId, request.getBelongType(), request.getBelongId());
AttachmentModuleRelation record = new AttachmentModuleRelation();
record.setRelationId(request.getBelongId());
record.setRelationType(request.getBelongType());
record.setAttachmentId(fileAttachmentMetadata.getId());
attachmentModuleRelationMapper.insert(record);
});
// 文件管理关联附件
List<AttachmentModuleRelation> refAttachments = attachmentModuleRelations.stream()
.filter(relation -> StringUtils.isNotEmpty(relation.getFileMetadataRefId())).collect(Collectors.toList());
refAttachments.forEach(refAttachment -> {
refAttachment.setRelationId(request.getBelongId());
attachmentModuleRelationMapper.insert(refAttachment);
// 缺陷类型的附件, 关联时需单独同步第三方平台
if (AttachmentType.ISSUE.type().equals(request.getBelongType())) {
String metadataRefId = getRefIdByAttachmentId(refAttachment.getAttachmentId());
FileMetadata fileMetadata = fileMetadataMapper.selectByPrimaryKey(metadataRefId);
IssuesWithBLOBs issues = issuesMapper.selectByPrimaryKey(request.getBelongId());
IssuesUpdateRequest updateRequest = new IssuesUpdateRequest();
updateRequest.setPlatformId(issues.getPlatformId());
File refFile = downloadMetadataFile(metadataRefId, fileMetadata.getName());
IssuesRequest issuesRequest = new IssuesRequest();
issuesRequest.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
Objects.requireNonNull(IssueFactory.createPlatform(issues.getPlatform(), issuesRequest))
.syncIssuesAttachment(updateRequest, refFile, AttachmentSyncType.UPLOAD);
FileUtils.deleteFile(FileUtils.ATTACHMENT_TMP_DIR + File.separator + fileMetadata.getName());
}
});
}
}
public List<FileAttachmentMetadata> listMetadata(AttachmentRequest request) {
List<String> attachmentIds = getAttachmentIdsByParam(request);
if (CollectionUtils.isEmpty(attachmentIds)) {
return new ArrayList<>();
List<FileAttachmentMetadata> attachments = new ArrayList<FileAttachmentMetadata>();
AttachmentModuleRelationExample example = new AttachmentModuleRelationExample();
example.createCriteria().andRelationIdEqualTo(request.getBelongId()).andRelationTypeEqualTo(request.getBelongType());
List<AttachmentModuleRelation> attachmentModuleRelations = attachmentModuleRelationMapper.selectByExample(example);
Map<String, String> relationMap = attachmentModuleRelations.stream()
.collect(Collectors.toMap(AttachmentModuleRelation::getAttachmentId,
relation -> relation.getFileMetadataRefId() == null ? "" : relation.getFileMetadataRefId()));
List<String> attachmentIds = attachmentModuleRelations.stream().map(AttachmentModuleRelation::getAttachmentId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(attachmentIds)) {
FileAttachmentMetadataExample fileExample = new FileAttachmentMetadataExample();
fileExample.createCriteria().andIdIn(attachmentIds);
List<FileAttachmentMetadata> fileAttachmentMetadata = fileAttachmentMetadataMapper.selectByExample(fileExample);
fileAttachmentMetadata.forEach(file -> {
String fileRefId = relationMap.get(file.getId());
if (StringUtils.isEmpty(fileRefId)) {
file.setIsLocal(Boolean.TRUE);
file.setIsRelatedDeleted(Boolean.FALSE);
} else {
file.setIsLocal(Boolean.FALSE);
FileAssociation fileAssociation = fileAssociationMapper.selectByPrimaryKey(fileRefId);
if (fileAssociation != null) {
// 关联文件信息同步
FileMetadata fileMetadata = fileMetadataMapper.selectByPrimaryKey(fileAssociation.getFileMetadataId());
file.setIsRelatedDeleted(Boolean.FALSE);
file.setName(fileMetadata.getName());
file.setSize(fileMetadata.getSize());
file.setCreator(fileMetadata.getCreateUser());
file.setCreateTime(fileMetadata.getCreateTime());
} else {
file.setIsRelatedDeleted(Boolean.TRUE);
}
}
});
attachments.addAll(fileAttachmentMetadata);
}
FileAttachmentMetadataExample fileExample = new FileAttachmentMetadataExample();
fileExample.createCriteria().andIdIn(attachmentIds);
return fileAttachmentMetadataMapper.selectByExample(fileExample);
return attachments;
}
public void relate(AttachmentRequest request) {
// 批量关联文件管理
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
FileAssociationMapper associationBatchMapper = sqlSession.getMapper(FileAssociationMapper.class);
AttachmentModuleRelationMapper attachmentModuleRelationBatchMapper = sqlSession.getMapper(AttachmentModuleRelationMapper.class);
FileAttachmentMetadataMapper fileAttachmentMetadataBatchMapper = sqlSession.getMapper(FileAttachmentMetadataMapper.class);
if (CollectionUtils.isNotEmpty(request.getMetadataRefIds())) {
request.getMetadataRefIds().forEach(metadataRefId -> {
FileMetadata fileMetadata = fileMetadataMapper.selectByPrimaryKey(metadataRefId);
FileAssociation fileAssociation = new FileAssociation();
fileAssociation.setId(UUID.randomUUID().toString());
fileAssociation.setFileMetadataId(metadataRefId);
fileAssociation.setFileType(fileMetadata.getType());
if (AttachmentType.TEST_CASE.type().equals(request.getBelongType())) {
fileAssociation.setType(FileAssociationType.TEST_CASE.name());
} else {
fileAssociation.setType(FileAssociationType.ISSUE.name());
}
fileAssociation.setProjectId(fileMetadata.getProjectId());
fileAssociation.setSourceItemId(metadataRefId);
fileAssociation.setSourceId(request.getBelongId());
associationBatchMapper.insert(fileAssociation);
AttachmentModuleRelation record = new AttachmentModuleRelation();
record.setRelationId(request.getBelongId());
record.setRelationType(request.getBelongType());
record.setFileMetadataRefId(fileAssociation.getId());
record.setAttachmentId(UUID.randomUUID().toString());
attachmentModuleRelationBatchMapper.insert(record);
FileAttachmentMetadata fileAttachmentMetadata = new FileAttachmentMetadata();
BeanUtils.copyBean(fileAttachmentMetadata, fileMetadata);
fileAttachmentMetadata.setId(record.getAttachmentId());
fileAttachmentMetadata.setCreator(fileMetadata.getCreateUser());
fileAttachmentMetadata.setFilePath(fileMetadata.getPath());
fileAttachmentMetadataBatchMapper.insert(fileAttachmentMetadata);
// 缺陷类型的附件, 关联时需单独同步第三方平台
if (AttachmentType.ISSUE.type().equals(request.getBelongType())) {
IssuesWithBLOBs issues = issuesMapper.selectByPrimaryKey(request.getBelongId());
IssuesUpdateRequest updateRequest = new IssuesUpdateRequest();
updateRequest.setPlatformId(issues.getPlatformId());
File refFile = downloadMetadataFile(metadataRefId, fileMetadata.getName());
IssuesRequest issuesRequest = new IssuesRequest();
issuesRequest.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
Objects.requireNonNull(IssueFactory.createPlatform(issues.getPlatform(), issuesRequest))
.syncIssuesAttachment(updateRequest, refFile, AttachmentSyncType.UPLOAD);
FileUtils.deleteFile(FileUtils.ATTACHMENT_TMP_DIR + File.separator + fileMetadata.getName());
}
});
sqlSession.flushStatements();
if (sqlSession != null && sqlSessionFactory != null) {
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
}
}
public void unrelated(AttachmentRequest request) {
// 缺陷类型的附件, 取消关联时同步第三方平台
if (AttachmentType.ISSUE.type().equals(request.getBelongType())) {
IssuesWithBLOBs issues = issuesMapper.selectByPrimaryKey(request.getBelongId());
request.getMetadataRefIds().forEach(metadataRefId -> {
FileAttachmentMetadata fileAttachmentMetadata = fileAttachmentMetadataMapper.selectByPrimaryKey(metadataRefId);
IssuesUpdateRequest updateRequest = new IssuesUpdateRequest();
updateRequest.setPlatformId(issues.getPlatformId());
File deleteFile = new File(FileUtils.ATTACHMENT_TMP_DIR + File.separator + fileAttachmentMetadata.getName());
IssuesRequest issuesRequest = new IssuesRequest();
issuesRequest.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
Objects.requireNonNull(IssueFactory.createPlatform(issues.getPlatform(), issuesRequest))
.syncIssuesAttachment(updateRequest, deleteFile, AttachmentSyncType.DELETE);
});
}
AttachmentModuleRelationExample example = new AttachmentModuleRelationExample();
example.createCriteria().andRelationIdEqualTo(request.getBelongId())
.andRelationTypeEqualTo(request.getBelongType())
.andAttachmentIdIn(request.getMetadataRefIds());
FileAttachmentMetadataExample exampleAttachment = new FileAttachmentMetadataExample();
exampleAttachment.createCriteria().andIdIn(request.getMetadataRefIds());
fileAttachmentMetadataMapper.deleteByExample(exampleAttachment);
attachmentModuleRelationMapper.deleteByExample(example);
}
public List<String> getAttachmentIdsByParam(AttachmentRequest request) {
@ -159,6 +311,15 @@ public class AttachmentService {
return attachmentIds;
}
public String getRefIdByAttachmentId(String attachmentId) {
AttachmentModuleRelationExample example = new AttachmentModuleRelationExample();
example.createCriteria().andAttachmentIdEqualTo(attachmentId);
List<AttachmentModuleRelation> relations = attachmentModuleRelationMapper.selectByExample(example);
String associationId = relations.get(0).getFileMetadataRefId();
FileAssociation fileAssociation = fileAssociationMapper.selectByPrimaryKey(associationId);
return fileAssociation.getFileMetadataId();
}
public void initAttachment() {
List<AttachmentModuleRelation> attachmentModuleRelations = new ArrayList<>();
List<IssueFile> issueFiles = issueFileMapper.selectByExample(new IssueFileExample());
@ -183,4 +344,9 @@ public class AttachmentService {
}
extAttachmentModuleRelationMapper.batchInsert(attachmentModuleRelations);
}
public File downloadMetadataFile(String fileMetadataRefId, String filename) {
byte[] refFileBytes = fileMetadataService.loadFileAsBytes(fileMetadataRefId);
return FileUtils.byteToFile(refFileBytes, FileUtils.ATTACHMENT_TMP_DIR, filename);
}
}

View File

@ -37,6 +37,10 @@ import io.metersphere.track.request.testcase.IssuesUpdateRequest;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@ -44,6 +48,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
@ -91,11 +96,15 @@ public class IssuesService {
@Resource
IssueFileMapper issueFileMapper;
@Resource
SqlSessionFactory sqlSessionFactory;
@Resource
private AttachmentService attachmentService;
@Resource
private CustomFieldService customFieldService;
@Resource
private ProjectMapper projectMapper;
@Resource
private FileMetadataMapper fileMetadataMapper;
private static final String SYNC_THIRD_PARTY_ISSUES_KEY = "ISSUE:SYNC";
@ -129,9 +138,9 @@ public class IssuesService {
attachmentRequest.setBelongType(AttachmentType.ISSUE.type());
attachmentService.copyAttachment(attachmentRequest);
} else {
final String issueId = issues.getId();
// 新增, 需保存并同步所有待上传的附件
if (CollectionUtils.isNotEmpty(files)) {
final String issueId = issues.getId();
files.forEach(file -> {
AttachmentRequest attachmentRequest = new AttachmentRequest();
attachmentRequest.setBelongId(issueId);
@ -139,6 +148,48 @@ public class IssuesService {
attachmentService.uploadAttachment(attachmentRequest, file);
});
}
// 处理待关联的文件附件, 生成关联记录, 并同步至第三方平台
if (CollectionUtils.isNotEmpty(issuesRequest.getRelateFileMetaIds())) {
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
FileAssociationMapper associationBatchMapper = sqlSession.getMapper(FileAssociationMapper.class);
AttachmentModuleRelationMapper attachmentModuleRelationBatchMapper = sqlSession.getMapper(AttachmentModuleRelationMapper.class);
FileAttachmentMetadataMapper fileAttachmentMetadataBatchMapper = sqlSession.getMapper(FileAttachmentMetadataMapper.class);
issuesRequest.getRelateFileMetaIds().forEach(filemetaId -> {
FileMetadata fileMetadata = fileMetadataMapper.selectByPrimaryKey(filemetaId);
FileAssociation fileAssociation = new FileAssociation();
fileAssociation.setId(UUID.randomUUID().toString());
fileAssociation.setFileMetadataId(filemetaId);
fileAssociation.setFileType(fileMetadata.getType());
fileAssociation.setType(FileAssociationType.ISSUE.name());
fileAssociation.setProjectId(fileMetadata.getProjectId());
fileAssociation.setSourceItemId(filemetaId);
fileAssociation.setSourceId(issueId);
associationBatchMapper.insert(fileAssociation);
AttachmentModuleRelation relation = new AttachmentModuleRelation();
relation.setRelationId(issueId);
relation.setRelationType(AttachmentType.ISSUE.type());
relation.setFileMetadataRefId(fileAssociation.getId());
relation.setAttachmentId(UUID.randomUUID().toString());
attachmentModuleRelationBatchMapper.insert(relation);
FileAttachmentMetadata fileAttachmentMetadata = new FileAttachmentMetadata();
BeanUtils.copyBean(fileAttachmentMetadata, fileMetadata);
fileAttachmentMetadata.setId(relation.getAttachmentId());
fileAttachmentMetadata.setCreator(fileMetadata.getCreateUser());
fileAttachmentMetadata.setFilePath(fileMetadata.getPath());
fileAttachmentMetadataBatchMapper.insert(fileAttachmentMetadata);
// 下载文件管理文件, 同步到第三方平台
File refFile = attachmentService.downloadMetadataFile(filemetaId, fileMetadata.getName());
IssuesRequest addIssueRequest = new IssuesRequest();
addIssueRequest.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
Objects.requireNonNull(IssueFactory.createPlatform(issuesRequest.getPlatform(), addIssueRequest))
.syncIssuesAttachment(issuesRequest, refFile, AttachmentSyncType.UPLOAD);
FileUtils.deleteFile(FileUtils.ATTACHMENT_TMP_DIR + File.separator + fileMetadata.getName());
});
sqlSession.flushStatements();
if (sqlSession != null && sqlSessionFactory != null) {
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
}
}
return getIssue(issues.getId());
}

View File

@ -19,6 +19,7 @@ import io.metersphere.api.service.ApiTestCaseService;
import io.metersphere.base.domain.*;
import io.metersphere.base.domain.ext.CustomFieldResource;
import io.metersphere.base.mapper.*;
import io.metersphere.base.mapper.ext.ExtAttachmentModuleRelationMapper;
import io.metersphere.base.mapper.ext.ExtIssuesMapper;
import io.metersphere.base.mapper.ext.ExtProjectVersionMapper;
import io.metersphere.base.mapper.ext.ExtTestCaseMapper;
@ -135,6 +136,8 @@ public class TestCaseService {
@Resource
AttachmentModuleRelationMapper attachmentModuleRelationMapper;
@Resource
ExtAttachmentModuleRelationMapper extAttachmentModuleRelationMapper;
@Resource
private LoadTestMapper loadTestMapper;
@Resource
private ApiScenarioMapper apiScenarioMapper;
@ -2101,6 +2104,41 @@ public class TestCaseService {
attachmentService.uploadAttachment(attachmentRequest, file);
});
}
// 同步待关联的文件附件, 生成关联记录
if (CollectionUtils.isNotEmpty(request.getRelateFileMetaIds())) {
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
FileAssociationMapper associationBatchMapper = sqlSession.getMapper(FileAssociationMapper.class);
AttachmentModuleRelationMapper attachmentModuleRelationBatchMapper = sqlSession.getMapper(AttachmentModuleRelationMapper.class);
FileAttachmentMetadataMapper fileAttachmentMetadataBatchMapper = sqlSession.getMapper(FileAttachmentMetadataMapper.class);
request.getRelateFileMetaIds().forEach(filemetaId -> {
FileMetadata fileMetadata = fileMetadataMapper.selectByPrimaryKey(filemetaId);
FileAssociation fileAssociation = new FileAssociation();
fileAssociation.setId(UUID.randomUUID().toString());
fileAssociation.setFileMetadataId(filemetaId);
fileAssociation.setFileType(fileMetadata.getType());
fileAssociation.setType(FileAssociationType.TEST_CASE.name());
fileAssociation.setProjectId(fileMetadata.getProjectId());
fileAssociation.setSourceItemId(filemetaId);
fileAssociation.setSourceId(testCaseWithBLOBs.getId());
associationBatchMapper.insert(fileAssociation);
AttachmentModuleRelation record = new AttachmentModuleRelation();
record.setRelationId(testCaseWithBLOBs.getId());
record.setRelationType(AttachmentType.TEST_CASE.type());
record.setFileMetadataRefId(fileAssociation.getId());
record.setAttachmentId(UUID.randomUUID().toString());
attachmentModuleRelationBatchMapper.insert(record);
FileAttachmentMetadata fileAttachmentMetadata = new FileAttachmentMetadata();
BeanUtils.copyBean(fileAttachmentMetadata, fileMetadata);
fileAttachmentMetadata.setId(record.getAttachmentId());
fileAttachmentMetadata.setCreator(fileMetadata.getCreateUser());
fileAttachmentMetadata.setFilePath(fileMetadata.getPath());
fileAttachmentMetadataBatchMapper.insert(fileAttachmentMetadata);
});
sqlSession.flushStatements();
if (sqlSession != null && sqlSessionFactory != null) {
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
}
}
return testCaseWithBLOBs;
}

View File

@ -1,12 +1,13 @@
package io.metersphere.xmind.utils;
import io.metersphere.commons.utils.FileUtils;
import io.metersphere.commons.utils.LogUtil;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.*;
/**
* 工具类
@ -47,6 +48,23 @@ public class FileUtil {
return null;
}
/**
* File MultipartFile
* @param file
* @return
*/
public static MultipartFile fileToMultipartFile(File file) {
DiskFileItem item = new DiskFileItem("file", MediaType.MULTIPART_FORM_DATA_VALUE, true,
file.getName(), (int)file.length(), file.getParentFile());
try {
OutputStream os = item.getOutputStream();
os.write(FileUtils.fileToByte(file));
} catch (IOException e) {
e.printStackTrace();
}
return new CommonsMultipartFile(item);
}
public static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();

View File

@ -151,3 +151,8 @@ ALTER TABLE `file_metadata` ADD INDEX `INDEX_REF_ID`(`ref_id`);
ALTER TABLE `ui_scenario` ADD COLUMN scenario_type VARCHAR(100) NOT NULL DEFAULT 'scenario' COMMENT 'Scenario type' AFTER `level`;
ALTER TABLE `ui_scenario_module` ADD COLUMN scenario_type VARCHAR(100) NOT NULL DEFAULT 'scenario' COMMENT 'Scenario type' AFTER `level`;
-- 功能用例, 缺陷管理文件管理关联字段
ALTER TABLE `attachment_module_relation` MODIFY COLUMN attachment_id VARCHAR(50) NULL;
ALTER TABLE `attachment_module_relation` ADD COLUMN file_metadata_ref_id VARCHAR(50) DEFAULT NULL COMMENT 'FILE ASSOCIATION ID';

View File

@ -2,13 +2,22 @@
<div>
<el-row type="flex" justify="center">
<el-col>
<el-table class="basic-config" :data="tableData">
<el-table class="basic-config" :data="tableData" :row-class-name="handleIsRelated">
<el-table-column
prop="name"
:label="$t('load_test.file_name')">
<template v-slot:default="scope">
<el-tooltip class="item" effect="dark" :content="scope.row.name" placement="top">
<el-progress
class="row-delete-name"
type="line"
v-if="!scope.row.isLocal && scope.row.isRelatedDeleted"
:stroke-width="40"
:text-inside="true"
:format="clearPercentage(scope.row)">
</el-progress>
<el-progress
v-else
:color="scope.row.progress >= 100 ? '' : uploadProgressColor"
type="line"
:format="clearPercentage(scope.row)"
@ -37,7 +46,7 @@
:width="70"
:label="$t('commons.status')">
<template v-slot:default="scope">
<span :class="scope.row.status === 'success' ? 'green' : scope.row.status === 'error' ? 'red' : scope.row.status === 'toUpload' ? 'yellow' : ''">{{ scope.row.status | formatStatus}}</span>
<span :class="scope.row.status === 'expired' ? 'lightgrey' : scope.row.status === 'success' ? 'green' : scope.row.status === 'error' ? 'red' : scope.row.status === 'toUpload' || 'toRelate' ? 'yellow' : ''">{{ formatStatus(scope.row.status) }}</span>
</template>
</el-table-column>
<el-table-column
@ -46,19 +55,27 @@
:label="$t('group.operator')">
</el-table-column>
<el-table-column
:width="140"
:width="180"
:label="$t('commons.operating')">
<template v-slot:default="scope">
<el-button @click="preview(scope.row)" :disabled="!scope.row.id" type="primary"
<el-button @click="preview(scope.row)" type="primary" :disabled="!scope.row.id || scope.row.status === 'toRelate' || scope.row.isRelatedDeleted"
v-if="scope.row.progress === 100 && isPreview(scope.row)"
icon="el-icon-view" size="mini" circle/>
<el-button @click="handleDownload(scope.row)" type="primary" :disabled="!scope.row.id"
<el-button @click="handleDownload(scope.row)" type="primary" :disabled="!scope.row.id || scope.row.status === 'toRelate' || scope.row.isRelatedDeleted"
v-if="scope.row.progress === 100"
icon="el-icon-download" size="mini" circle/>
<el-button :disabled="isCopy || !scope.row.id"
@click="handleUpload(scope.row)" type="primary"
v-if="scope.row.progress === 100 && scope.row.isLocal"
icon="el-icon-upload" size="mini" circle/>
<el-button :disabled="readOnly || !isDelete || isCopy || (!scope.row.id && scope.row.status !== 'toUpload')"
@click="handleDelete(scope.row, scope.$index)" type="danger"
v-if="scope.row.progress === 100"
icon="el-icon-delete" size="mini"
v-if="scope.row.progress === 100 && scope.row.isLocal"
icon="el-icon-delete" size="mini" circle/>
<el-button :disabled="readOnly || !isDelete || isCopy || (!scope.row.id && scope.row.status !== 'toRelate')"
@click="handleUnRelate(scope.row, scope.$index)" type="danger"
v-if="scope.row.progress === 100 && !scope.row.isLocal"
icon="el-icon-unlock" size="mini"
circle/>
<el-button :disabled="readOnly || !isDelete" @click="handleCancel(scope.row, scope.$index)" type="danger"
v-if="scope.row.progress < 100"
@ -100,10 +117,14 @@ export default {
data() {
return {
uploadProgressColor: '#d4f6d4',
uploadSuccessColor: '#FFFFFF'
uploadSuccessColor: '#FFFFFF',
that: null,
}
},
methods: {
handleIsRelated(row, rowIndex) {
return row.row.isRelatedDeleted ? 'delete-row' : '';
},
clearPercentage(row) {
return () => {
return row.name;
@ -120,19 +141,35 @@ export default {
this.$fileDownloadPost('/attachment/download', {
name: file.name,
id: file.id,
isLocal: file.isLocal
});
},
handleUpload(file) {
this.$emit("handleDump", file);
},
handleDelete(file, index) {
this.$emit("handleDelete", file, index);
},
handleUnRelate(file, index) {
this.$emit("handleUnRelate", file, index);
},
handleCancel(file, index) {
this.$emit("handleCancel", file, index);
},
},
filters: {
formatStatus(status) {
if (isNaN(status)) {
return status === 'success' ? '完成' : status === 'toUpload' ? '待上传' : '失败'
switch (status) {
case 'success':
return this.$t('commons.file_upload_status.success');
case 'toUpload':
return this.$t('commons.file_upload_status.to_upload');
case 'toRelate':
return this.$t('commons.file_upload_status.to_relate');
case 'expired':
return this.$t('commons.file_upload_status.expired');
default:
return this.$t('commons.file_upload_status.error');
}
}
return Math.floor(status * 100 / 100) + "%";
}
@ -145,6 +182,10 @@ export default {
color: black;
}
::v-deep .el-progress.row-delete-name .el-progress-bar__innerText {
color: lightgrey!important;
}
::v-deep .el-progress-bar__outer,
::v-deep .el-progress-bar__inner {
border-radius: inherit ;
@ -169,4 +210,14 @@ export default {
.yellow {
color: #E6A23C;
}
.lightgrey {
color: lightgrey;
}
</style>
<style>
.el-table .delete-row {
color: lightgrey;
}
</style>

View File

@ -742,6 +742,9 @@ export default {
if (this.validate(param)) {
let option = this.getOption(param);
this.result = this.$request(option, (response) => {
//
this.currentTestCaseInfo.isCopy = false;
this.$refs.otherInfo.getFileMetaData(response.data.id);
this.$success(this.$t('commons.save_success'));
this.path = "/test/case/edit";
// this.operationType = "edit"
@ -767,8 +770,6 @@ export default {
if (callback) {
callback(this);
}
//
//
if (hasLicense()) {
this.getVersionHistory();
@ -810,6 +811,12 @@ export default {
if (this.selectedOtherInfo) {
param.otherInfoConfig = this.selectedOtherInfo;
}
if (this.$refs.otherInfo.relateFiles.length > 0) {
param.relateFileMetaIds = this.$refs.otherInfo.relateFiles;
}
if (this.$refs.otherInfo.unRelateFiles.length > 0) {
param.unRelateFileMetaIds = this.$refs.otherInfo.unRelateFiles;
}
return param;
},
parseOldFields(param) {

View File

@ -60,23 +60,33 @@
<el-tab-pane :label="$t('test_track.case.attachment')" name="attachment">
<el-row>
<el-col :span="22">
<el-upload
multiple
:limit="8"
action=""
:auto-upload="true"
:file-list="fileList"
:show-file-list="false"
:before-upload="beforeUpload"
:http-request="handleUpload"
:on-exceed="handleExceed"
:on-success="handleSuccess"
:on-error="handleError"
:disabled="readOnly || isCopy">
<el-button :disabled="readOnly || isCopy" type="primary" size="mini">{{$t('test_track.case.add_attachment')}}</el-button>
<el-col :span="22" style="margin-bottom: 10px;">
<div class="upload-default" @click.stop>
<el-popover placement="right" trigger="hover">
<div>
<el-upload
multiple
:limit="8"
action=""
:auto-upload="true"
:file-list="fileList"
:show-file-list="false"
:before-upload="beforeUpload"
:http-request="handleUpload"
:on-exceed="handleExceed"
:on-success="handleSuccess"
:on-error="handleError"
:disabled="readOnly || isCopy">
<el-button :disabled="readOnly || isCopy" type="text">{{$t('permission.project_file.local_upload')}}</el-button>
</el-upload>
</div>
<el-button type="text" :disabled="readOnly || isCopy" @click="associationFile">{{ $t('permission.project_file.associated_files') }}</el-button>
<i class="el-icon-plus" slot="reference"/>
</el-popover>
</div>
<div :class="readOnly ? 'testplan-local-upload-tip' : 'not-testplan-local-upload-tip'">
<span slot="tip" class="el-upload__tip"> {{ $t('test_track.case.upload_tip') }} </span>
</el-upload>
</div>
</el-col>
</el-row>
<el-row>
@ -86,6 +96,8 @@
:is-copy="isCopy"
:is-delete="!isTestPlan"
@handleDelete="handleDelete"
@handleUnRelate="handleUnRelate"
@handleDump="handleDump"
@handleCancel="handleCancel"/>
</el-col>
</el-row>
@ -121,6 +133,8 @@
</el-col>
</el-row>
</el-tab-pane>
<ms-file-metadata-list ref="metadataList" @checkRows="checkRows"/>
<ms-file-batch-move ref="module" @setModuleId="setModuleId"/>
</el-tabs>
</template>
@ -137,10 +151,12 @@ import TabPaneCount from "@/business/components/track/plan/view/comonents/report
import {getRelationshipCountCase} from "@/network/testCase";
import TestCaseComment from "@/business/components/track/case/components/TestCaseComment";
import ReviewCommentItem from "@/business/components/track/review/commom/ReviewCommentItem";
import {byteToSize, getTypeByFileName, hasLicense} from "@/common/js/utils";
import {byteToSize, getCurrentProjectID, getTypeByFileName, getUUID, hasLicense} from "@/common/js/utils";
import {TokenKey} from "@/common/js/constants";
import axios from "axios";
import {validateAndSetLicense} from "@/business/permission";
import MsFileMetadataList from "@/business/components/project/menu/file/quote/QuoteFileList";
import MsFileBatchMove from "@/business/components/project/menu/file/module/FileBatchMove";
export default {
name: "TestCaseEditOtherInfo",
@ -150,6 +166,8 @@ export default {
TestCaseTestRelate,
TestCaseComment,
ReviewCommentItem,
MsFileMetadataList,
MsFileBatchMove,
FormRichTextItem, TestCaseIssueRelate, TestCaseAttachment, MsRichText, TestCaseRichText
},
props: ['form', 'labelWidth', 'caseId', 'readOnly', 'projectId', 'isTestPlan', 'planId', 'versionEnable', 'isCopy', 'copyCaseId',
@ -174,7 +192,10 @@ export default {
},
intervalMap: new Map(),
cancelFileToken: [],
uploadFiles: []
uploadFiles: [],
relateFiles: [],
unRelateFiles: [],
dumpFile: {},
};
},
computed: {
@ -262,7 +283,8 @@ export default {
progress: this.type === 'add' ? 100 : 0,
status: this.type === 'add' ? 'toUpload' : 0,
creator: user.name,
type: getTypeByFileName(file.name)
type: getTypeByFileName(file.name),
isLocal: true
});
if (this.type === 'add') {
@ -365,7 +387,8 @@ export default {
this.fileList.splice(index, 1);
this.tableData.splice(index, 1);
if (this.type === 'add') {
this.uploadFiles.splice(index, 1);
let delIndex = this.uploadFiles.findIndex(uploadFile => uploadFile.name === file.name)
this.uploadFiles.splice(delIndex, 1);
} else {
this.$get('/attachment/delete/testcase/' + file.id , response => {
this.$success(this.$t('commons.delete_success'));
@ -373,6 +396,36 @@ export default {
});
}
},
handleUnRelate(file, index) {
//
this.$alert(this.$t('load_test.unrelated_file_confirm') + file.name + "?", '', {
confirmButtonText: this.$t('commons.confirm'),
dangerouslyUseHTMLString: true,
callback: (action) => {
if (action === 'confirm') {
let unRelateFileIndex = this.tableData.findIndex(f => f.name === file.name);
this.tableData.splice(unRelateFileIndex, 1);
if (file.status === 'toRelate') {
// ,
let unRelateId = this.relateFiles.findIndex(f => f === file.id);
this.relateFiles.splice(unRelateId, 1);
} else {
//
this.unRelateFiles.push(file.id);
let data = {'belongType': 'testcase', 'belongId': this.caseId, 'metadataRefIds': this.unRelateFiles};
this.$post('/attachment/metadata/unrelated', data, response => {
this.$success(this.$t('commons.unrelated_success'));
this.getFileMetaData();
});
}
}
}
});
},
handleDump(file) {
this.$refs.module.init();
this.dumpFile = file;
},
handleCancel(file, index) {
this.fileList.splice(index, 1);
let cancelToken = this.cancelFileToken.filter(f => f.name === file.name)[0];
@ -382,13 +435,17 @@ export default {
cancelFile.status = 'error';
},
getFileMetaData(id) {
if (this.type === 'edit') {
this.relateFiles = [];
this.unRelateFiles = [];
}
this.$emit("update:isClickAttachmentTab", true);
// id
this.fileList = [];
this.tableData = [];
let testCaseId;
if (this.isCopy) {
testCaseId = this.copyCaseId
testCaseId = id ? id : this.copyCaseId
} else {
testCaseId = id ? id : this.caseId;
}
@ -404,12 +461,61 @@ export default {
this.tableData = JSON.parse(JSON.stringify(files));
this.tableData.map(f => {
f.size = byteToSize(f.size);
f.status = 'success';
f.status = f.isRelatedDeleted ? 'expired' : 'success';
f.progress = 100
});
});
}
},
associationFile() {
this.$refs.metadataList.open();
},
checkRows(rows) {
let repeatRecord = false;
for (let row of rows) {
let rowIndex = this.tableData.findIndex(item => item.name === row.name);
if (rowIndex >= 0) {
this.$error(this.$t('load_test.exist_related_file') + ": " + row.name);
repeatRecord = true;
break;
}
}
if (!repeatRecord) {
if (this.type === 'add') {
//
rows.forEach(row => {
this.relateFiles.push(row.id);
this.tableData.push({
id: row.id,
name: row.name,
size: byteToSize(row.size),
updateTime: row.createTime,
progress: 100,
status: 'toRelate',
creator: row.createUser,
type: row.type,
isLocal: false,
});
})
} else {
//
let metadataRefIds = [];
rows.forEach(row => metadataRefIds.push(row.id));
let data = {'belongType': 'testcase', 'belongId': this.caseId, 'metadataRefIds': metadataRefIds};
this.$post('/attachment/metadata/relate', data, response => {
this.$success(this.$t('commons.relate_success'));
this.getFileMetaData();
});
}
}
},
setModuleId(moduleId) {
let data = {id: getUUID(), resourceId: getCurrentProjectID(), moduleId: moduleId,
projectId: getCurrentProjectID(), fileName: this.dumpFile.name, attachmentId: this.dumpFile.id};
this.$post("/attachment/metadata/dump", data, (response) => {
this.$success(this.$t("organization.integration.successful_operation"));
});
},
getRelatedTest() {
this.$refs.relateTest.initTable();
},
@ -510,4 +616,45 @@ export default {
.demandInput {
width: 200px;
}
.el-icon-plus {
font-size: 16px;
}
.upload-default {
background-color: #fbfdff;
border: 1px dashed #c0ccda;
border-radius: 6px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 40px;
height: 30px;
line-height: 32px;
vertical-align: top;
text-align: center;
cursor: pointer;
display: inline-block;
}
.upload-default i {
color: #8c939d;
}
.upload-default:hover {
border: 1px dashed #783887;
}
.testplan-local-upload-tip {
display: inline-block;
position: relative;
left: 25px;
top: -5px;
}
.not-testplan-local-upload-tip {
display: inline-block;
position: relative;
left: 25px;
top: 8px;
}
</style>

View File

@ -1,10 +1,10 @@
<template>
<el-dialog :visible.sync="dialogVisible" width="80%" :destroy-on-close="true" :before-close="close" :append-to-body="true">
<div>
<img :src="'/attachment/preview/' + file.id" :alt="$t('test_track.case.img_loading_fail')" style="width: 100%;height: 100%;"
<img :src="'/attachment/preview/' + file.id + '/' + file.isLocal" :alt="$t('test_track.case.img_loading_fail')" style="width: 100%;height: 100%;"
v-if="file.type === 'JPG' || file.type === 'JPEG' || file.type === 'PNG'">
<div v-if="file.type === 'PDF'">
<test-case-pdf :file-id="file.id"/>
<test-case-pdf :file-id="file.id" :is-local="file.isLocal"/>
</div>
</div>
</el-dialog>

View File

@ -10,7 +10,8 @@ export default {
name: "TestCasePdf",
components: {pdf},
props: {
fileId: String
fileId: String,
isLocal: Boolean
},
data() {
return {
@ -21,7 +22,7 @@ export default {
},
mounted() {
this.loading = true;
this.loadingTask = pdf.createLoadingTask("/attachment/preview/" + this.fileId);
this.loadingTask = pdf.createLoadingTask("/attachment/preview/" + this.fileId + "/" + this.isLocal);
this.loadingTask.promise.then(pdf => {
this.numPages = pdf.numPages
this.loading = false;

View File

@ -102,23 +102,33 @@
<el-tab-pane :label="$t('test_track.case.attachment')" name="attachment">
<el-row>
<el-col :span="22">
<el-upload
multiple
:limit="8"
action=""
:auto-upload="true"
:file-list="fileList"
:show-file-list="false"
:before-upload="beforeUpload"
:http-request="handleUpload"
:on-exceed="handleExceed"
:on-success="handleSuccess"
:on-error="handleError"
:disabled="type === 'copy'">
<el-button type="primary" :disabled="type === 'copy'" size="mini">{{$t('test_track.case.add_attachment')}}</el-button>
<el-col :span="22" style="margin-bottom: 10px;">
<div class="upload-default" @click.stop>
<el-popover placement="right" trigger="hover">
<div>
<el-upload
multiple
:limit="8"
action=""
:auto-upload="true"
:file-list="fileList"
:show-file-list="false"
:before-upload="beforeUpload"
:http-request="handleUpload"
:on-exceed="handleExceed"
:on-success="handleSuccess"
:on-error="handleError"
:disabled="readOnly || type === 'copy'">
<el-button :disabled="readOnly || type === 'copy'" type="text">{{$t('permission.project_file.local_upload')}}</el-button>
</el-upload>
</div>
<el-button type="text" :disabled="readOnly || type === 'copy'" @click="associationFile">{{ $t('permission.project_file.associated_files') }}</el-button>
<i class="el-icon-plus" slot="reference"/>
</el-popover>
</div>
<div class="local-upload-tips">
<span slot="tip" class="el-upload__tip"> {{ $t('test_track.case.upload_tip') }} </span>
</el-upload>
</div>
</el-col>
</el-row>
<el-row style="margin-top: 10px">
@ -128,7 +138,9 @@
:is-delete="isDelete"
:is-copy="type === 'copy'"
@handleDelete="handleDelete"
@handleCancel="handleCancel"/>
@handleCancel="handleCancel"
@handleUnRelate="handleUnRelate"
@handleDump="handleDump"/>
</el-col>
</el-row>
</el-tab-pane>
@ -170,9 +182,10 @@
<issue-comment :issues-id="form.id"
@getComments="getComments"
ref="issueComment"/>
<ms-file-metadata-list ref="metadataList" @checkRows="checkRows"/>
<ms-file-batch-move ref="module" @setModuleId="setModuleId"/>
</el-form>
</el-scrollbar>
</el-main>
</template>
@ -193,7 +206,7 @@ import {
getCurrentProjectID,
getCurrentUser,
getCurrentUserId,
getCurrentWorkspaceId, getTypeByFileName, hasLicense,
getCurrentWorkspaceId, getTypeByFileName, getUUID, hasLicense,
} from "@/common/js/utils";
import {enableThirdPartTemplate, getIssuePartTemplateWithProject, getPlatformTransitions} from "@/network/Issue";
import CustomFiledFormItem from "@/business/components/common/components/form/CustomFiledFormItem";
@ -204,6 +217,8 @@ import {TokenKey} from "@/common/js/constants";
import {Message} from "element-ui";
import TestCaseAttachment from "@/business/components/track/case/components/TestCaseAttachment";
import axios from "axios";
import MsFileMetadataList from "@/business/components/project/menu/file/quote/QuoteFileList";
import MsFileBatchMove from "@/business/components/project/menu/file/module/FileBatchMove";
const {getIssuesById} = require("@/network/Issue");
@ -222,7 +237,9 @@ export default {
MsMarkDownText,
IssueComment,
ReviewCommentItem,
TestCaseAttachment
TestCaseAttachment,
MsFileMetadataList,
MsFileBatchMove,
},
data() {
return {
@ -308,7 +325,10 @@ export default {
readOnly: false,
isDelete: true,
cancelFileToken: [],
uploadFiles: []
uploadFiles: [],
relateFiles: [],
unRelateFiles: [],
dumpFile: {},
};
},
props: {
@ -362,6 +382,7 @@ export default {
}
},
open(data, type) {
this.uploadFiles = [];
this.tabActiveName = 'relateTestCase'
this.showFollow = false;
this.result.loading = true;
@ -546,8 +567,10 @@ export default {
}
param.withoutTestCaseIssue = this.isMinder;
param.thirdPartPlatform = this.enableThirdPartTemplate;
if (this.relateFiles.length > 0) {
param.relateFileMetaIds = this.relateFiles;
}
return param;
},
_save() {
@ -651,7 +674,8 @@ export default {
progress: this.type === 'add' || this.isCaseEdit? 100 : 0,
status: this.type === 'add' || this.isCaseEdit? 'toUpload' : 0,
creator: user.name,
type: getTypeByFileName(file.name)
type: getTypeByFileName(file.name),
isLocal: true
});
if (this.type === 'add' || this.isCaseEdit) {
@ -753,7 +777,8 @@ export default {
this.fileList.splice(index, 1);
this.tableData.splice(index, 1);
if (this.type === 'add' || this.isCaseEdit) {
this.uploadFiles.splice(index, 1);
let delIndex = this.uploadFiles.findIndex(uploadFile => uploadFile.name === file.name)
this.uploadFiles.splice(delIndex, 1);
} else {
this.$get('/attachment/delete/issue/' + file.id , response => {
this.$success(this.$t('commons.delete_success'));
@ -761,6 +786,38 @@ export default {
});
}
},
handleUnRelate(file, index) {
//
this.$alert(this.$t('load_test.unrelated_file_confirm') + file.name + "?", '', {
confirmButtonText: this.$t('commons.confirm'),
dangerouslyUseHTMLString: true,
callback: (action) => {
if (action === 'confirm') {
let unRelateFileIndex = this.tableData.findIndex(f => f.name === file.name);
this.tableData.splice(unRelateFileIndex, 1);
if (file.status === 'toRelate') {
// ,
let unRelateId = this.relateFiles.findIndex(f => f === file.id);
this.relateFiles.splice(unRelateId, 1);
} else {
//
this.unRelateFiles.push(file.id);
let data = {'belongType': 'issue', 'belongId': this.issueId, 'metadataRefIds': this.unRelateFiles};
this.result.loading = true;
this.$post('/attachment/metadata/unrelated', data, response => {
this.$success(this.$t('commons.unrelated_success'));
this.result.loading = false;
this.getFileMetaData(this.issueId);
});
}
}
}
});
},
handleDump(file) {
this.$refs.module.init();
this.dumpFile = file;
},
handleCancel(file, index) {
this.fileList.splice(index, 1);
let cancelToken = this.cancelFileToken.filter(f => f.name === file.name)[0];
@ -769,7 +826,63 @@ export default {
cancelFile.progress = 100;
cancelFile.status = 'error';
},
associationFile() {
this.$refs.metadataList.open();
},
checkRows(rows) {
let repeatRecord = false;
for (let row of rows) {
let rowIndex = this.tableData.findIndex(item => item.name === row.name);
if (rowIndex >= 0) {
this.$error(this.$t('load_test.exist_related_file') + ": " + row.name);
repeatRecord = true;
break;
}
}
if (!repeatRecord) {
if (this.type === 'add') {
//
rows.forEach(row => {
this.relateFiles.push(row.id);
this.tableData.push({
id: row.id,
name: row.name,
size: byteToSize(row.size),
updateTime: row.createTime,
progress: 100,
status: 'toRelate',
creator: row.createUser,
type: row.type,
isLocal: false,
});
})
} else {
//
let metadataRefIds = [];
rows.forEach(row => metadataRefIds.push(row.id));
let data = {'belongType': 'issue', 'belongId': this.issueId, 'metadataRefIds': metadataRefIds};
this.result.loading = true;
this.$post('/attachment/metadata/relate', data, response => {
this.$success(this.$t('commons.relate_success'));
this.result.loading = false;
this.getFileMetaData(this.issueId);
});
}
}
},
setModuleId(moduleId) {
let data = {id: getUUID(), resourceId: getCurrentProjectID(), moduleId: moduleId,
projectId: getCurrentProjectID(), fileName: this.dumpFile.name, attachmentId: this.dumpFile.id};
this.$post("/attachment/metadata/dump", data, (response) => {
this.$success(this.$t("organization.integration.successful_operation"));
});
},
getFileMetaData(id) {
if (this.type === 'edit') {
this.uploadFiles = [];
this.relateFiles = [];
this.unRelateFiles = [];
}
// id
this.fileList = [];
this.tableData = [];
@ -838,4 +951,38 @@ export default {
font-size: xx-small;
border-radius: 50%;
}
.el-icon-plus {
font-size: 16px;
}
.upload-default {
background-color: #fbfdff;
border: 1px dashed #c0ccda;
border-radius: 6px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 40px;
height: 30px;
line-height: 32px;
vertical-align: top;
text-align: center;
cursor: pointer;
display: inline-block;
}
.upload-default i {
color: #8c939d;
}
.upload-default:hover {
border: 1px dashed #783887;
}
.local-upload-tips {
display: inline-block;
position: relative;
left: 25px;
top: 8px;
}
</style>

View File

@ -72,6 +72,8 @@ export default {
warning_module_add: "Tree modules are up to 8 levels deep",
send_success: 'Send successfully',
delete_success: 'Deleted successfully',
relate_success: 'Related successfully',
unrelated_success: 'Unrelated successfully',
modify_success: 'Modify Success',
copy_success: 'Copy Success',
delete_cancel: 'Deleted Cancel',
@ -188,6 +190,13 @@ export default {
month: "Month",
year: "Year"
},
file_upload_status: {
success: 'Success',
to_upload: 'To upload',
to_relate: 'To be associated',
expired: 'Expired',
error: 'Error'
},
test_unit: 'tests',
remove: 'Remove',
next_level: "Next level",
@ -1098,6 +1107,7 @@ export default {
related_file_not_found: "No related test file found!",
delete_file_when_uploading: 'The current operation may interrupt the file being uploaded!',
delete_file_confirm: 'Confirm delete file:',
unrelated_file_confirm: 'Confirm unrelated file: ',
file_size_out_of_bounds: "File size out of bounds, file name: ",
file_size_limit: "The number of files exceeds the limit",
delete_file: "The file already exists, please delete the file with the same name first!",
@ -1160,6 +1170,7 @@ export default {
report_type: 'Report type',
upload_jmx: 'Upload JMX',
exist_jmx: 'Existed Files',
exist_related_file: 'Existed Relate Files',
other_resource: 'Resource Files',
upload_file: 'Upload Files',
load_exist_file: 'Load Project Files',

View File

@ -137,7 +137,7 @@ export default {
cancel_relevance_project: "Disassociating the project will also cancel the associated test cases under the project",
img_loading_fail: "Image failed to load",
pdf_loading_fail: "PDF loading failed",
upload_tip: "file size limit[0-500MB]",
upload_tip: "Local upload file size is limited to [0-500MB]",
add_attachment: "Add",
attachment: "Attachment",
upload_time: "Upload Time",

View File

@ -128,7 +128,7 @@ export default {
cancel_relevance_project: "取消项目关联会同时取消该项目下已关联的测试用例",
img_loading_fail: "图片加载失败",
pdf_loading_fail: "PDF加载失败",
upload_tip: "文件大小限制[0-500MB]",
upload_tip: "本地上传, 文件大小限制[0-500MB]",
add_attachment: "添加",
attachment: "附件",
upload_time: "上传时间",

View File

@ -128,7 +128,7 @@ export default {
cancel_relevance_project: "取消項目關聯會同時取消該項目下已關聯的測試用例",
img_loading_fail: "圖片加載失敗",
pdf_loading_fail: "PDF加載失敗",
upload_tip: "文件大小限制[0-500MB]",
upload_tip: "本地上傳, 文件大小限制[0-500MB]",
add_attachment: "添加",
attachment: "附件",
upload_time: "上傳時間",

View File

@ -71,6 +71,8 @@ export default {
warning_module_add: "模块树深度最大为8层",
send_success: '发送成功',
delete_success: '删除成功',
relate_success: '关联成功',
unrelated_success: '取消关联成功',
copy_success: '复制成功',
modify_success: '修改成功',
delete_cancel: '已取消删除',
@ -182,6 +184,13 @@ export default {
month: "月",
year: "年"
},
file_upload_status: {
success: '完成',
to_upload: '待上传',
to_relate: '待关联',
expired: '已失效',
error: '失败'
},
test_unit: '测试',
system_parameter_setting: '系统参数设置',
connection_successful: '连接成功',
@ -1106,6 +1115,7 @@ export default {
related_file_not_found: "未找到关联的测试文件!",
delete_file_when_uploading: '当前操作可能会中断正在上传的文件!',
delete_file_confirm: '确认删除文件: ',
unrelated_file_confirm: '确认取消关联: ',
file_size_limit: "文件个数超出限制!",
file_size_out_of_bounds: "文件大小超出范围, 文件名称: ",
delete_file: "文件已存在,请先删除同名文件!",
@ -1171,6 +1181,7 @@ export default {
report_type: "报告类型",
upload_jmx: '上传 JMX 文件',
exist_jmx: '已存在的文件',
exist_related_file: '已存在的关联文件',
other_resource: '资源文件',
upload_file: '上传新文件',
load_exist_file: '加载文件',

View File

@ -70,6 +70,8 @@ export default {
save_success: '保存成功',
send_success: '發送成功',
delete_success: '刪除成功',
relate_success: '關聯成功',
unrelated_success: '取消關聯成功',
copy_success: '復製成功',
warning_module_add: "模塊樹深度最大為8層",
modify_success: '修改成功',
@ -182,6 +184,13 @@ export default {
month: "月",
year: "年"
},
file_upload_status: {
success: '完成',
to_upload: '待上傳',
to_relate: '待關聯',
expired: '已失效',
error: '失敗'
},
test_unit: '測試',
system_parameter_setting: '系統參數設置',
connection_successful: '連接成功',
@ -1102,6 +1111,7 @@ export default {
related_file_not_found: "未找到關聯的測試文件!",
delete_file_when_uploading: '當前操作可能會中斷正在上傳的文件!',
delete_file_confirm: '確認刪除文件: ',
unrelated_file_confirm: '確認取消關聯: ',
file_size_limit: "文件個數超出限製!",
file_size_out_of_bounds: "文件大小超出範圍, 文件名称: ",
delete_file: "文件已存在,請先刪除同名文件!",
@ -1167,6 +1177,7 @@ export default {
report_type: "报告类型",
upload_jmx: '上傳 JMX 文件',
exist_jmx: '已存在的文件',
exist_related_file: '已存在的關聯文件',
other_resource: '資源文件',
upload_file: '上傳新文件',
load_exist_file: '加載文件',