diff --git a/backend/src/main/java/io/metersphere/base/domain/TestCaseFile.java b/backend/src/main/java/io/metersphere/base/domain/TestCaseFile.java new file mode 100644 index 0000000000..a0791bee07 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/domain/TestCaseFile.java @@ -0,0 +1,13 @@ +package io.metersphere.base.domain; + +import java.io.Serializable; +import lombok.Data; + +@Data +public class TestCaseFile implements Serializable { + private String caseId; + + private String fileId; + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/domain/TestCaseFileExample.java b/backend/src/main/java/io/metersphere/base/domain/TestCaseFileExample.java new file mode 100644 index 0000000000..ed55a15442 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/domain/TestCaseFileExample.java @@ -0,0 +1,340 @@ +package io.metersphere.base.domain; + +import java.util.ArrayList; +import java.util.List; + +public class TestCaseFileExample { + protected String orderByClause; + + protected boolean distinct; + + protected List oredCriteria; + + public TestCaseFileExample() { + oredCriteria = new ArrayList(); + } + + public void setOrderByClause(String orderByClause) { + this.orderByClause = orderByClause; + } + + public String getOrderByClause() { + return orderByClause; + } + + public void setDistinct(boolean distinct) { + this.distinct = distinct; + } + + public boolean isDistinct() { + return distinct; + } + + public List getOredCriteria() { + return oredCriteria; + } + + public void or(Criteria criteria) { + oredCriteria.add(criteria); + } + + public Criteria or() { + Criteria criteria = createCriteriaInternal(); + oredCriteria.add(criteria); + return criteria; + } + + public Criteria createCriteria() { + Criteria criteria = createCriteriaInternal(); + if (oredCriteria.size() == 0) { + oredCriteria.add(criteria); + } + return criteria; + } + + protected Criteria createCriteriaInternal() { + Criteria criteria = new Criteria(); + return criteria; + } + + public void clear() { + oredCriteria.clear(); + orderByClause = null; + distinct = false; + } + + protected abstract static class GeneratedCriteria { + protected List criteria; + + protected GeneratedCriteria() { + super(); + criteria = new ArrayList(); + } + + public boolean isValid() { + return criteria.size() > 0; + } + + public List getAllCriteria() { + return criteria; + } + + public List getCriteria() { + return criteria; + } + + protected void addCriterion(String condition) { + if (condition == null) { + throw new RuntimeException("Value for condition cannot be null"); + } + criteria.add(new Criterion(condition)); + } + + protected void addCriterion(String condition, Object value, String property) { + if (value == null) { + throw new RuntimeException("Value for " + property + " cannot be null"); + } + criteria.add(new Criterion(condition, value)); + } + + protected void addCriterion(String condition, Object value1, Object value2, String property) { + if (value1 == null || value2 == null) { + throw new RuntimeException("Between values for " + property + " cannot be null"); + } + criteria.add(new Criterion(condition, value1, value2)); + } + + public Criteria andCaseIdIsNull() { + addCriterion("case_id is null"); + return (Criteria) this; + } + + public Criteria andCaseIdIsNotNull() { + addCriterion("case_id is not null"); + return (Criteria) this; + } + + public Criteria andCaseIdEqualTo(String value) { + addCriterion("case_id =", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdNotEqualTo(String value) { + addCriterion("case_id <>", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdGreaterThan(String value) { + addCriterion("case_id >", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdGreaterThanOrEqualTo(String value) { + addCriterion("case_id >=", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdLessThan(String value) { + addCriterion("case_id <", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdLessThanOrEqualTo(String value) { + addCriterion("case_id <=", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdLike(String value) { + addCriterion("case_id like", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdNotLike(String value) { + addCriterion("case_id not like", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdIn(List values) { + addCriterion("case_id in", values, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdNotIn(List values) { + addCriterion("case_id not in", values, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdBetween(String value1, String value2) { + addCriterion("case_id between", value1, value2, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdNotBetween(String value1, String value2) { + addCriterion("case_id not between", value1, value2, "caseId"); + return (Criteria) this; + } + + public Criteria andFileIdIsNull() { + addCriterion("file_id is null"); + return (Criteria) this; + } + + public Criteria andFileIdIsNotNull() { + addCriterion("file_id is not null"); + return (Criteria) this; + } + + public Criteria andFileIdEqualTo(String value) { + addCriterion("file_id =", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdNotEqualTo(String value) { + addCriterion("file_id <>", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdGreaterThan(String value) { + addCriterion("file_id >", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdGreaterThanOrEqualTo(String value) { + addCriterion("file_id >=", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdLessThan(String value) { + addCriterion("file_id <", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdLessThanOrEqualTo(String value) { + addCriterion("file_id <=", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdLike(String value) { + addCriterion("file_id like", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdNotLike(String value) { + addCriterion("file_id not like", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdIn(List values) { + addCriterion("file_id in", values, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdNotIn(List values) { + addCriterion("file_id not in", values, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdBetween(String value1, String value2) { + addCriterion("file_id between", value1, value2, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdNotBetween(String value1, String value2) { + addCriterion("file_id not between", value1, value2, "fileId"); + return (Criteria) this; + } + } + + public static class Criteria extends GeneratedCriteria { + + protected Criteria() { + super(); + } + } + + public static class Criterion { + private String condition; + + private Object value; + + private Object secondValue; + + private boolean noValue; + + private boolean singleValue; + + private boolean betweenValue; + + private boolean listValue; + + private String typeHandler; + + public String getCondition() { + return condition; + } + + public Object getValue() { + return value; + } + + public Object getSecondValue() { + return secondValue; + } + + public boolean isNoValue() { + return noValue; + } + + public boolean isSingleValue() { + return singleValue; + } + + public boolean isBetweenValue() { + return betweenValue; + } + + public boolean isListValue() { + return listValue; + } + + public String getTypeHandler() { + return typeHandler; + } + + protected Criterion(String condition) { + super(); + this.condition = condition; + this.typeHandler = null; + this.noValue = true; + } + + protected Criterion(String condition, Object value, String typeHandler) { + super(); + this.condition = condition; + this.value = value; + this.typeHandler = typeHandler; + if (value instanceof List) { + this.listValue = true; + } else { + this.singleValue = true; + } + } + + protected Criterion(String condition, Object value) { + this(condition, value, null); + } + + protected Criterion(String condition, Object value, Object secondValue, String typeHandler) { + super(); + this.condition = condition; + this.value = value; + this.secondValue = secondValue; + this.typeHandler = typeHandler; + this.betweenValue = true; + } + + protected Criterion(String condition, Object value, Object secondValue) { + this(condition, value, secondValue, null); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.java b/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.java new file mode 100644 index 0000000000..b7eb9c0df4 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.java @@ -0,0 +1,22 @@ +package io.metersphere.base.mapper; + +import io.metersphere.base.domain.TestCaseFile; +import io.metersphere.base.domain.TestCaseFileExample; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface TestCaseFileMapper { + long countByExample(TestCaseFileExample example); + + int deleteByExample(TestCaseFileExample example); + + int insert(TestCaseFile record); + + int insertSelective(TestCaseFile record); + + List selectByExample(TestCaseFileExample example); + + int updateByExampleSelective(@Param("record") TestCaseFile record, @Param("example") TestCaseFileExample example); + + int updateByExample(@Param("record") TestCaseFile record, @Param("example") TestCaseFileExample example); +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.xml new file mode 100644 index 0000000000..70ef1820a9 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + and ${criterion.condition} + + + and ${criterion.condition} #{criterion.value} + + + and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} + + + and ${criterion.condition} + + #{listItem} + + + + + + + + + + + + + + + + + + and ${criterion.condition} + + + and ${criterion.condition} #{criterion.value} + + + and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} + + + and ${criterion.condition} + + #{listItem} + + + + + + + + + + + case_id, file_id + + + + delete from test_case_file + + + + + + insert into test_case_file (case_id, file_id) + values (#{caseId,jdbcType=VARCHAR}, #{fileId,jdbcType=VARCHAR}) + + + insert into test_case_file + + + case_id, + + + file_id, + + + + + #{caseId,jdbcType=VARCHAR}, + + + #{fileId,jdbcType=VARCHAR}, + + + + + + update test_case_file + + + case_id = #{record.caseId,jdbcType=VARCHAR}, + + + file_id = #{record.fileId,jdbcType=VARCHAR}, + + + + + + + + update test_case_file + set case_id = #{record.caseId,jdbcType=VARCHAR}, + file_id = #{record.fileId,jdbcType=VARCHAR} + + + + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/commons/constants/FileType.java b/backend/src/main/java/io/metersphere/commons/constants/FileType.java index db8236c2ed..2172e2e78e 100644 --- a/backend/src/main/java/io/metersphere/commons/constants/FileType.java +++ b/backend/src/main/java/io/metersphere/commons/constants/FileType.java @@ -1,7 +1,7 @@ package io.metersphere.commons.constants; public enum FileType { - JMX(".jmx"), CSV(".csv"), JSON(".json"); + JMX(".jmx"), CSV(".csv"), JSON(".json"), PDF(".pdf"), JPG(".jpg"), PNG(".png"); // 保存后缀 private String suffix; diff --git a/backend/src/main/java/io/metersphere/service/FileService.java b/backend/src/main/java/io/metersphere/service/FileService.java index b82c2f784d..ff2801320e 100644 --- a/backend/src/main/java/io/metersphere/service/FileService.java +++ b/backend/src/main/java/io/metersphere/service/FileService.java @@ -4,6 +4,7 @@ import io.metersphere.base.domain.*; import io.metersphere.base.mapper.FileContentMapper; import io.metersphere.base.mapper.FileMetadataMapper; import io.metersphere.base.mapper.LoadTestFileMapper; +import io.metersphere.base.mapper.TestCaseFileMapper; import io.metersphere.commons.constants.FileType; import io.metersphere.commons.exception.MSException; import org.springframework.stereotype.Service; @@ -24,6 +25,8 @@ public class FileService { private LoadTestFileMapper loadTestFileMapper; @Resource private FileContentMapper fileContentMapper; + @Resource + private TestCaseFileMapper testCaseFileMapper; public byte[] loadFileAsBytes(String id) { FileContent fileContent = fileContentMapper.selectByPrimaryKey(id); @@ -66,6 +69,19 @@ public class FileService { loadTestFileMapper.deleteByExample(example3); } + public void deleteFileRelatedByIds(List ids) { + if (CollectionUtils.isEmpty(ids)) { + return; + } + FileMetadataExample example = new FileMetadataExample(); + example.createCriteria().andIdIn(ids); + fileMetadataMapper.deleteByExample(example); + + FileContentExample example2 = new FileContentExample(); + example2.createCriteria().andFileIdIn(ids); + fileContentMapper.deleteByExample(example2); + } + public FileMetadata saveFile(MultipartFile file) { final FileMetadata fileMetadata = new FileMetadata(); fileMetadata.setId(UUID.randomUUID().toString()); @@ -109,4 +125,19 @@ public class FileService { String type = filename.substring(s); return FileType.valueOf(type.toUpperCase()); } + + public List getFileMetadataByCaseId(String caseId) { + TestCaseFileExample testCaseFileExample = new TestCaseFileExample(); + testCaseFileExample.createCriteria().andCaseIdEqualTo(caseId); + final List testCaseFiles = testCaseFileMapper.selectByExample(testCaseFileExample); + + if (CollectionUtils.isEmpty(testCaseFiles)) { + return null; + } + + List fileIds = testCaseFiles.stream().map(TestCaseFile::getFileId).collect(Collectors.toList()); + FileMetadataExample example = new FileMetadataExample(); + example.createCriteria().andIdIn(fileIds); + return fileMetadataMapper.selectByExample(example); + } } \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java b/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java index 3833985e07..d6df272082 100644 --- a/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java +++ b/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java @@ -2,6 +2,7 @@ package io.metersphere.track.controller; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; +import io.metersphere.base.domain.FileMetadata; import io.metersphere.base.domain.Project; import io.metersphere.base.domain.TestCase; import io.metersphere.base.domain.TestCaseWithBLOBs; @@ -11,12 +12,18 @@ import io.metersphere.commons.utils.Pager; import io.metersphere.commons.utils.SessionUtils; import io.metersphere.excel.domain.ExcelResponse; import io.metersphere.service.CheckOwnerService; +import io.metersphere.service.FileService; import io.metersphere.track.dto.TestCaseDTO; +import io.metersphere.track.request.testcase.EditTestCaseRequest; import io.metersphere.track.request.testcase.QueryTestCaseRequest; import io.metersphere.track.request.testcase.TestCaseBatchRequest; +import io.metersphere.track.request.testplan.FileOperationRequest; import io.metersphere.track.service.TestCaseService; import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -33,6 +40,8 @@ public class TestCaseController { TestCaseService testCaseService; @Resource private CheckOwnerService checkOwnerService; + @Resource + private FileService fileService; @PostMapping("/list/{goPage}/{pageSize}") public Pager> list(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody QueryTestCaseRequest request) { @@ -94,16 +103,16 @@ public class TestCaseController { return testCaseService.getProjectByTestCaseId(testCaseId); } - @PostMapping("/add") + @PostMapping(value = "/add", consumes = {"multipart/form-data"}) @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) - public void addTestCase(@RequestBody TestCaseWithBLOBs testCase) { - testCaseService.addTestCase(testCase); + public void addTestCase(@RequestPart("request") TestCaseWithBLOBs testCase, @RequestPart(value = "file") List files) { + testCaseService.save(testCase, files); } - @PostMapping("/edit") + @PostMapping(value = "/edit", consumes = {"multipart/form-data"}) @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) - public void editTestCase(@RequestBody TestCaseWithBLOBs testCase) { - testCaseService.editTestCase(testCase); + public void editTestCase(@RequestPart("request") EditTestCaseRequest request, @RequestPart(value = "file") List files) { + testCaseService.edit(request, files); } @PostMapping("/delete/{testCaseId}") @@ -150,4 +159,18 @@ public class TestCaseController { testCaseService.deleteTestCaseBath(request); } + @GetMapping("/file/metadata/{caseId}") + public List getFileMetadata(@PathVariable String caseId) { + return fileService.getFileMetadataByCaseId(caseId); + } + + @PostMapping("/file/download") + public ResponseEntity downloadJmx(@RequestBody FileOperationRequest fileOperationRequest) { + byte[] bytes = fileService.loadFileAsBytes(fileOperationRequest.getId()); + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType("application/octet-stream")) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileOperationRequest.getName() + "\"") + .body(bytes); + } + } diff --git a/backend/src/main/java/io/metersphere/track/request/testcase/EditTestCaseRequest.java b/backend/src/main/java/io/metersphere/track/request/testcase/EditTestCaseRequest.java new file mode 100644 index 0000000000..3cdc82df3a --- /dev/null +++ b/backend/src/main/java/io/metersphere/track/request/testcase/EditTestCaseRequest.java @@ -0,0 +1,14 @@ +package io.metersphere.track.request.testcase; + +import io.metersphere.base.domain.FileMetadata; +import io.metersphere.base.domain.TestCaseWithBLOBs; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class EditTestCaseRequest extends TestCaseWithBLOBs { + private List updatedFileList; +} diff --git a/backend/src/main/java/io/metersphere/track/service/TestCaseService.java b/backend/src/main/java/io/metersphere/track/service/TestCaseService.java index eb477a2740..8af7931100 100644 --- a/backend/src/main/java/io/metersphere/track/service/TestCaseService.java +++ b/backend/src/main/java/io/metersphere/track/service/TestCaseService.java @@ -26,10 +26,13 @@ import io.metersphere.excel.listener.EasyExcelListener; import io.metersphere.excel.listener.TestCaseDataListener; import io.metersphere.excel.utils.EasyExcelExporter; import io.metersphere.i18n.Translator; +import io.metersphere.service.FileService; import io.metersphere.track.dto.TestCaseDTO; +import io.metersphere.track.request.testcase.EditTestCaseRequest; import io.metersphere.track.request.testcase.QueryTestCaseRequest; import io.metersphere.track.request.testcase.TestCaseBatchRequest; import io.metersphere.xmind.XmindCaseParser; +import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.session.SqlSession; @@ -83,8 +86,12 @@ public class TestCaseService { TestCaseReviewTestCaseMapper testCaseReviewTestCaseMapper; @Resource TestCaseCommentService testCaseCommentService; + @Resource + FileService fileService; + @Resource + TestCaseFileMapper testCaseFileMapper; - public void addTestCase(TestCaseWithBLOBs testCase) { + private TestCaseWithBLOBs addTestCase(TestCaseWithBLOBs testCase) { testCase.setName(testCase.getName()); checkTestCaseExist(testCase); testCase.setId(UUID.randomUUID().toString()); @@ -93,6 +100,7 @@ public class TestCaseService { testCase.setNum(getNextNum(testCase.getProjectId())); testCase.setReviewStatus(TestCaseReviewStatus.Prepare.name()); testCaseMapper.insert(testCase); + return testCase; } public List getTestCaseByNodeId(List nodeIds) { @@ -596,4 +604,55 @@ public class TestCaseService { return false; } + + public String save(TestCaseWithBLOBs testCase, List files) { + if (files == null) { + throw new IllegalArgumentException(Translator.get("file_cannot_be_null")); + } + final TestCaseWithBLOBs testCaseWithBLOBs = addTestCase(testCase); + files.forEach(file -> { + final FileMetadata fileMetadata = fileService.saveFile(file); + TestCaseFile testCaseFile = new TestCaseFile(); + testCaseFile.setCaseId(testCaseWithBLOBs.getId()); + testCaseFile.setFileId(fileMetadata.getId()); + testCaseFileMapper.insert(testCaseFile); + }); + return testCaseWithBLOBs.getId(); + } + + public String edit(EditTestCaseRequest request, List files) { + TestCaseWithBLOBs testCaseWithBLOBs = testCaseMapper.selectByPrimaryKey(request.getId()); + if (testCaseWithBLOBs == null) { + MSException.throwException(Translator.get("edit_load_test_not_found") + request.getId()); + } + + // 新选择了一个文件,删除原来的文件 + List updatedFiles = request.getUpdatedFileList(); + List originFiles = fileService.getFileMetadataByCaseId(request.getId()); + List updatedFileIds = updatedFiles.stream().map(FileMetadata::getId).collect(Collectors.toList()); + List originFileIds = originFiles.stream().map(FileMetadata::getId).collect(Collectors.toList()); + // 相减 + List deleteFileIds = ListUtils.subtract(originFileIds, updatedFileIds); + fileService.deleteFileRelatedByIds(deleteFileIds); + + if (!CollectionUtils.isEmpty(deleteFileIds)) { + TestCaseFileExample testCaseFileExample = new TestCaseFileExample(); + testCaseFileExample.createCriteria().andFileIdIn(deleteFileIds); + testCaseFileMapper.deleteByExample(testCaseFileExample); + } + + + if (files != null) { + files.forEach(file -> { + final FileMetadata fileMetadata = fileService.saveFile(file); + TestCaseFile testCaseFile = new TestCaseFile(); + testCaseFile.setFileId(fileMetadata.getId()); + testCaseFile.setCaseId(request.getId()); + testCaseFileMapper.insert(testCaseFile); + }); + } + + editTestCase(request); + return request.getId(); + } } diff --git a/backend/src/main/java/io/metersphere/xpack b/backend/src/main/java/io/metersphere/xpack index cf6b065263..ee74568be0 160000 --- a/backend/src/main/java/io/metersphere/xpack +++ b/backend/src/main/java/io/metersphere/xpack @@ -1 +1 @@ -Subproject commit cf6b06526324326a563d933e07118fac014a63b4 +Subproject commit ee74568be0beba46da19616f5832e83f9164c688 diff --git a/backend/src/main/resources/db/migration/V30__test_case_file.sql b/backend/src/main/resources/db/migration/V30__test_case_file.sql new file mode 100644 index 0000000000..f4b2c4a3d4 --- /dev/null +++ b/backend/src/main/resources/db/migration/V30__test_case_file.sql @@ -0,0 +1,7 @@ +create table if not exists test_case_file +( + case_id varchar(64) null, + file_id varchar(64) null, + constraint test_case_file_unique_key + unique (case_id, file_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/frontend/src/business/components/track/case/components/TestCaseEdit.vue b/frontend/src/business/components/track/case/components/TestCaseEdit.vue index cff54cc231..169bd88a16 100644 --- a/frontend/src/business/components/track/case/components/TestCaseEdit.vue +++ b/frontend/src/business/components/track/case/components/TestCaseEdit.vue @@ -209,6 +209,59 @@ + + + 附件: + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/business/components/xpack b/frontend/src/business/components/xpack index 06d935cd1d..cc38137a69 160000 --- a/frontend/src/business/components/xpack +++ b/frontend/src/business/components/xpack @@ -1 +1 @@ -Subproject commit 06d935cd1d22ab36f09763745c2aff8ad3fb08c1 +Subproject commit cc38137a69a0f20fadece9c0f9f50a9468c4ace9