reafactor: markdown 替换富文本框
This commit is contained in:
parent
9b884c168b
commit
4e677b5e3b
|
@ -415,7 +415,8 @@
|
|||
|
||||
<select id="listForMinder" resultType="io.metersphere.base.domain.TestCaseWithBLOBs">
|
||||
select
|
||||
id, `name`, node_id, node_path, `type`, `method`, maintainer, priority, prerequisite, remark, steps
|
||||
<include refid="io.metersphere.base.mapper.TestCaseMapper.Base_Column_List"/>,
|
||||
<include refid="io.metersphere.base.mapper.TestCaseMapper.Blob_Column_List"/>
|
||||
from test_case
|
||||
<include refid="queryWhereCondition"/>
|
||||
<include refid="io.metersphere.base.mapper.ext.ExtBaseMapper.orders"/>
|
||||
|
|
|
@ -4,9 +4,17 @@ import io.metersphere.commons.exception.MSException;
|
|||
import io.metersphere.i18n.Translator;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.aspectj.util.FileUtil;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.*;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class FileUtils {
|
||||
|
@ -31,4 +39,36 @@ public class FileUtils {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String uploadFile(MultipartFile uploadFile, String path, String name) {
|
||||
if (uploadFile == null) {
|
||||
return null;
|
||||
}
|
||||
File testDir = new File(path);
|
||||
if (!testDir.exists()) {
|
||||
testDir.mkdirs();
|
||||
}
|
||||
String filePath = testDir + "/" + name;
|
||||
File file = new File(filePath);
|
||||
try (InputStream in = uploadFile.getInputStream(); OutputStream out = new FileOutputStream(file)) {
|
||||
file.createNewFile();
|
||||
FileUtil.copyStream(in, out);
|
||||
} catch (IOException e) {
|
||||
LogUtil.error(e.getMessage(), e);
|
||||
MSException.throwException(Translator.get("upload_fail"));
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public static String uploadFile(MultipartFile uploadFile, String path) {
|
||||
return uploadFile(uploadFile, path, uploadFile.getOriginalFilename());
|
||||
}
|
||||
|
||||
public static void deleteFile(String path) {
|
||||
File file = new File(path);
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ public class ShiroUtils {
|
|||
filterChainDefinitionMap.put("/document", "apikey, authc"); // 跳转到 /document 不用校验 csrf
|
||||
filterChainDefinitionMap.put("/test/case/file/preview/**", "apikey, authc"); // 预览测试用例附件 不用校验 csrf
|
||||
filterChainDefinitionMap.put("/mock", "apikey, authc"); // 跳转到 /mock接口 不用校验 csrf
|
||||
filterChainDefinitionMap.put("/resource/md/get/**", "apikey, authc");
|
||||
}
|
||||
|
||||
public static Cookie getSessionIdCookie(){
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package io.metersphere.controller;
|
||||
|
||||
import io.metersphere.commons.constants.RoleConstants;
|
||||
import io.metersphere.controller.request.MdUploadRequest;
|
||||
import io.metersphere.service.ResourceService;
|
||||
import org.apache.shiro.authz.annotation.Logical;
|
||||
import org.apache.shiro.authz.annotation.RequiresRoles;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = "/resource")
|
||||
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER}, logical = Logical.OR)
|
||||
public class ResourceController {
|
||||
|
||||
@Resource
|
||||
ResourceService resourceService;
|
||||
@PostMapping(value = "/md/upload", consumes = {"multipart/form-data"})
|
||||
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER,}, logical = Logical.OR)
|
||||
public void upload(@RequestPart(value = "request") MdUploadRequest request, @RequestPart(value = "file") MultipartFile file) {
|
||||
resourceService.mdUpload(request, file);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/md/get/{fileName}")
|
||||
public ResponseEntity<FileSystemResource> getFile(@PathVariable("fileName") String fileName) {
|
||||
return resourceService.getMdImage(fileName);
|
||||
}
|
||||
|
||||
@GetMapping("/md/delete/{fileName}")
|
||||
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER,}, logical = Logical.OR)
|
||||
public void delete(@PathVariable("fileName") String fileName) {
|
||||
resourceService.mdDelete(fileName);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package io.metersphere.controller.request;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class MdUploadRequest {
|
||||
private String id;
|
||||
}
|
|
@ -5,18 +5,19 @@ import io.metersphere.base.domain.JarConfig;
|
|||
import io.metersphere.base.domain.JarConfigExample;
|
||||
import io.metersphere.base.mapper.JarConfigMapper;
|
||||
import io.metersphere.commons.exception.MSException;
|
||||
import io.metersphere.commons.utils.LogUtil;
|
||||
import io.metersphere.commons.utils.FileUtils;
|
||||
import io.metersphere.commons.utils.SessionUtils;
|
||||
import io.metersphere.i18n.Translator;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.util.FileUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
|
@ -65,7 +66,7 @@ public class JarConfigService {
|
|||
|
||||
public void delete(String id) {
|
||||
JarConfig JarConfig = jarConfigMapper.selectByPrimaryKey(id);
|
||||
deleteJarFile(JarConfig.getPath());
|
||||
FileUtils.deleteFile(JarConfig.getPath());
|
||||
jarConfigMapper.deleteByPrimaryKey(id);
|
||||
}
|
||||
|
||||
|
@ -81,8 +82,8 @@ public class JarConfigService {
|
|||
}
|
||||
jarConfigMapper.updateByPrimaryKey(jarConfig);
|
||||
if (file != null) {
|
||||
deleteJarFile(deletePath);
|
||||
createJarFiles(file);
|
||||
FileUtils.deleteFile(deletePath);
|
||||
FileUtils.uploadFile(file, JAR_FILE_DIR);
|
||||
NewDriverManager.loadJar(jarConfig.getPath());
|
||||
}
|
||||
}
|
||||
|
@ -98,50 +99,15 @@ public class JarConfigService {
|
|||
jarConfig.setPath(getJarPath(file));
|
||||
jarConfig.setFileName(file.getOriginalFilename());
|
||||
jarConfigMapper.insert(jarConfig);
|
||||
createJarFiles(file);
|
||||
FileUtils.uploadFile(file, JAR_FILE_DIR);
|
||||
NewDriverManager.loadJar(jarConfig.getPath());
|
||||
return jarConfig.getId();
|
||||
}
|
||||
|
||||
public void deleteJarFiles(String testId) {
|
||||
File file = new File(JAR_FILE_DIR + "/" + testId);
|
||||
FileUtil.deleteContents(file);
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteJarFile(String path) {
|
||||
File file = new File(path);
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public String getJarPath(MultipartFile file) {
|
||||
return JAR_FILE_DIR + "/" + file.getOriginalFilename();
|
||||
}
|
||||
|
||||
private String createJarFiles(MultipartFile jar) {
|
||||
if (jar == null) {
|
||||
return null;
|
||||
}
|
||||
File testDir = new File(JAR_FILE_DIR);
|
||||
if (!testDir.exists()) {
|
||||
testDir.mkdirs();
|
||||
}
|
||||
String filePath = testDir + "/" + jar.getOriginalFilename();
|
||||
File file = new File(filePath);
|
||||
try (InputStream in = jar.getInputStream(); OutputStream out = new FileOutputStream(file)) {
|
||||
file.createNewFile();
|
||||
FileUtil.copyStream(in, out);
|
||||
} catch (IOException e) {
|
||||
LogUtil.error(e.getMessage(), e);
|
||||
MSException.throwException(Translator.get("upload_fail"));
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private void checkExist(JarConfig jarConfig) {
|
||||
if (jarConfig.getName() != null) {
|
||||
JarConfigExample example = new JarConfigExample();
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package io.metersphere.service;
|
||||
|
||||
import io.metersphere.commons.utils.FileUtils;
|
||||
import io.metersphere.controller.request.MdUploadRequest;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public class ResourceService {
|
||||
|
||||
// private static final String RESOURCE_DIR = "/opt/metersphere/data/resource/";
|
||||
private static final String MD_IMAGE_DIR = "/opt/metersphere/data/image/markdown";
|
||||
|
||||
public void mdUpload(MdUploadRequest request, MultipartFile file) {
|
||||
FileUtils.uploadFile(file, MD_IMAGE_DIR, request.getId() + "_" + file.getOriginalFilename());
|
||||
}
|
||||
|
||||
public ResponseEntity<FileSystemResource> getMdImage(String name) {
|
||||
File file = new File(MD_IMAGE_DIR + "/" + name);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
headers.add("Content-Disposition", "attachment; filename=" + file.getName());
|
||||
headers.add("Pragma", "no-cache");
|
||||
headers.add("Expires", "0");
|
||||
headers.add("Last-Modified", new Date().toString());
|
||||
headers.add("ETag", String.valueOf(System.currentTimeMillis()));
|
||||
return ResponseEntity.ok()
|
||||
.headers(headers)
|
||||
.contentLength(file.length())
|
||||
.contentType(MediaType.parseMediaType("application/octet-stream"))
|
||||
.body(new FileSystemResource(file));
|
||||
}
|
||||
|
||||
public void mdDelete(String fileName) {
|
||||
FileUtils.deleteFile(MD_IMAGE_DIR + "/" + fileName);
|
||||
}
|
||||
}
|
|
@ -39,6 +39,7 @@
|
|||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isobject": "^3.0.2",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"mavon-editor": "^2.9.1",
|
||||
"md5": "^2.3.0",
|
||||
"mockjs": "^1.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
|
|
|
@ -1,23 +1,84 @@
|
|||
<template>
|
||||
<el-form-item :disable="true" :label="title" :prop="prop" :label-width="labelWidth">
|
||||
<test-case-rich-text :disabled="disabled" :content="data[prop]" @updateRichText="updateData"/>
|
||||
<!-- <test-case-rich-text :disabled="disabled" :content="data[prop]" @updateRichText="updateData"/>-->
|
||||
<mavon-editor :editable="!disabled" @imgAdd="imgAdd" :default-open="disabled ? 'preview' : null" class="mavon-editor"
|
||||
:subfield="disabled ? false : true" :toolbars="toolbars" @imgDel="imgDel" v-model="data[prop]" ref="md"/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TestCaseRichText from "@/business/components/track/case/components/MsRichText";
|
||||
import {getUUID} from "@/common/js/utils";
|
||||
export default {
|
||||
name: "FormRichTextItem",
|
||||
components: {TestCaseRichText},
|
||||
components: {},
|
||||
props: ['data', 'title', 'prop', 'disabled', 'labelWidth'],
|
||||
data() {
|
||||
return {
|
||||
toolbars: {
|
||||
bold: true, // 粗体
|
||||
italic: true, // 斜体
|
||||
header: true, // 标题
|
||||
underline: true, // 下划线
|
||||
strikethrough: true, // 中划线
|
||||
mark: true, // 标记
|
||||
superscript: true, // 上角标
|
||||
subscript: true, // 下角标
|
||||
quote: true, // 引用
|
||||
ol: true, // 有序列表
|
||||
ul: true, // 无序列表
|
||||
link: true, // 链接
|
||||
imagelink: true, // 图片链接
|
||||
code: true, // code
|
||||
table: true, // 表格
|
||||
fullscreen: true, // 全屏编辑
|
||||
readmodel: true, // 沉浸式阅读
|
||||
htmlcode: true, // 展示html源码
|
||||
help: true, // 帮助
|
||||
/* 1.3.5 */
|
||||
undo: true, // 上一步
|
||||
redo: true, // 下一步
|
||||
trash: true, // 清空
|
||||
save: false, // 保存(触发events中的save事件)
|
||||
/* 1.4.2 */
|
||||
navigation: true, // 导航目录
|
||||
/* 2.1.8 */
|
||||
alignleft: true, // 左对齐
|
||||
aligncenter: true, // 居中
|
||||
alignright: true, // 右对齐
|
||||
/* 2.2.1 */
|
||||
subfield: true, // 单双栏模式
|
||||
preview: true, // 预览
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData(value) {
|
||||
this.data[this.prop] = value;
|
||||
}
|
||||
},
|
||||
imgAdd(pos, file){
|
||||
let param = {
|
||||
id: getUUID().substring(0, 8)
|
||||
};
|
||||
file.prefix = param.id;
|
||||
this.result = this.$fileUpload('/resource/md/upload', file, null, param, () => {
|
||||
this.$success(this.$t('commons.save_success'));
|
||||
this.$refs.md.$img2Url(pos, '/resource/md/get/' + param.id + '_' + file.name);
|
||||
});
|
||||
this.$emit('imgAdd', file);
|
||||
},
|
||||
imgDel(file) {
|
||||
if (file) {
|
||||
this.$get('/resource/md/delete/' + file[1].prefix + "_" + file[1].name);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.mavon-editor {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -79,8 +79,6 @@
|
|||
|
||||
<test-case-step-item :label-width="formLabelWidth" v-if="form.stepModel === 'STEP'" :form="form" :read-only="readOnly"/>
|
||||
|
||||
<ms-form-divider :title="$t('test_track.case.other_info')"/>
|
||||
|
||||
<test-case-edit-other-info :project-id="projectIds" :form="form" :label-width="formLabelWidth" :case-id="form.id" ref="otherInfo"/>
|
||||
|
||||
<el-row style="margin-top: 10px" v-if="type!='add'">
|
||||
|
@ -217,8 +215,8 @@ export default {
|
|||
maintainer: [{required: true, message: this.$t('test_track.case.input_maintainer'), trigger: 'change'}],
|
||||
priority: [{required: true, message: this.$t('test_track.case.input_priority'), trigger: 'change'}],
|
||||
method: [{required: true, message: this.$t('test_track.case.input_method'), trigger: 'change'}],
|
||||
prerequisite: [{max: 500, message: this.$t('test_track.length_less_than') + '500', trigger: 'blur'}],
|
||||
remark: [{max: 1000, message: this.$t('test_track.length_less_than') + '1000', trigger: 'blur'}]
|
||||
// prerequisite: [{max: 500, message: this.$t('test_track.length_less_than') + '500', trigger: 'blur'}],
|
||||
// remark: [{max: 1000, message: this.$t('test_track.length_less_than') + '1000', trigger: 'blur'}]
|
||||
},
|
||||
customFieldRules: {},
|
||||
customFieldForm: {},
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<el-tabs class="other-info-tabs" v-loading="result.loading" v-model="tabActiveName">
|
||||
<el-tab-pane :label="$t('commons.remark')" name="remark">
|
||||
<el-row>
|
||||
<ms-rich-text :disabled="readOnly" :content="form.remark" @updateRichText="updateRemark"/>
|
||||
<form-rich-text-item class="remark-item" :disabled="readOnly" :data="form" prop="remark"/>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('test_track.case.relate_test')" name="relateTest">
|
||||
|
@ -89,10 +89,11 @@ import {TEST} from "@/business/components/api/definition/model/JsonData";
|
|||
import TestCaseAttachment from "@/business/components/track/case/components/TestCaseAttachment";
|
||||
import TestCaseIssueRelate from "@/business/components/track/case/components/TestCaseIssueRelate";
|
||||
import {enableModules} from "@/common/js/utils";
|
||||
import FormRichTextItem from "@/business/components/track/case/components/FormRichTextItem";
|
||||
|
||||
export default {
|
||||
name: "TestCaseEditOtherInfo",
|
||||
components: {TestCaseIssueRelate, TestCaseAttachment, MsRichText, TestCaseRichText},
|
||||
components: {FormRichTextItem, TestCaseIssueRelate, TestCaseAttachment, MsRichText, TestCaseRichText},
|
||||
props: ['form', 'labelWidth', 'caseId', 'readOnly', 'projectId', 'isTestPlan'],
|
||||
data() {
|
||||
return {
|
||||
|
@ -314,4 +315,8 @@ export default {
|
|||
.other-info-tabs {
|
||||
padding: 10px 60px;
|
||||
}
|
||||
|
||||
.remark-item {
|
||||
padding: 0px 15px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -156,6 +156,11 @@ name: "TestCaseMinder",
|
|||
step.result = result;
|
||||
}
|
||||
steps.push(step);
|
||||
|
||||
if (data.stepModel === 'TEXT') {
|
||||
testCase.stepDescription = step.desc;
|
||||
testCase.expectedResult = step.result;
|
||||
}
|
||||
}
|
||||
if (childData.changed) isChange = true;
|
||||
})
|
||||
|
|
|
@ -11,11 +11,13 @@ export function getTestCaseDataMap(testCase, isDisable, setParamCallback) {
|
|||
}
|
||||
|
||||
export function parseCase(item, dataMap, isDisable, setParamCallback) {
|
||||
|
||||
if (item.steps) {
|
||||
item.steps = JSON.parse(item.steps);
|
||||
} else {
|
||||
item.steps = [];
|
||||
}
|
||||
|
||||
// if (item.tags && item.tags.length > 0) {
|
||||
// item.tags = JSON.parse(item.tags);
|
||||
// }
|
||||
|
@ -28,7 +30,8 @@ export function parseCase(item, dataMap, isDisable, setParamCallback) {
|
|||
resource: [i18n.t('api_test.definition.request.case')],
|
||||
type: item.type,
|
||||
method: item.method,
|
||||
maintainer: item.maintainer
|
||||
maintainer: item.maintainer,
|
||||
stepModel: item.stepModel
|
||||
}
|
||||
}
|
||||
if (setParamCallback) {
|
||||
|
@ -54,15 +57,23 @@ function parseChildren(nodeItem, item, isDisable) {
|
|||
nodeItem.children = [];
|
||||
let children = [];
|
||||
_parseChildren(children, item.prerequisite, i18n.t('test_track.case.prerequisite'), isDisable);
|
||||
if (item.steps) {
|
||||
item.steps.forEach((step) => {
|
||||
let descNode = _parseChildren(children, step.desc, undefined, isDisable);
|
||||
if (descNode) {
|
||||
descNode.data.num = step.num;
|
||||
descNode.children = [];
|
||||
_parseChildren(descNode.children, step.result, undefined, isDisable);
|
||||
}
|
||||
});
|
||||
if (item.stepModel === 'TEXT') {
|
||||
let descNode = _parseChildren(children, item.stepDescription, null, isDisable);
|
||||
if (descNode) {
|
||||
descNode.children = [];
|
||||
_parseChildren(descNode.children, item.expectedResult, null, isDisable);
|
||||
}
|
||||
} else {
|
||||
if (item.steps) {
|
||||
item.steps.forEach((step) => {
|
||||
let descNode = _parseChildren(children, step.desc, undefined, isDisable);
|
||||
if (descNode) {
|
||||
descNode.data.num = step.num;
|
||||
descNode.children = [];
|
||||
_parseChildren(descNode.children, step.result, undefined, isDisable);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
_parseChildren(children, item.remark, i18n.t('commons.remark'), isDisable);
|
||||
nodeItem.children = children;
|
||||
|
|
|
@ -23,6 +23,11 @@ import JsonSchemaEditor from './components/common/json-schema/schema/index';
|
|||
import JSONPathPicker from 'vue-jsonpath-picker';
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
import vueMinderEditor from 'vue-minder-editor-plus'
|
||||
|
||||
import mavonEditor from 'mavon-editor'
|
||||
import 'mavon-editor/dist/css/index.css'
|
||||
Vue.use(mavonEditor)
|
||||
|
||||
Vue.use(vueMinderEditor)
|
||||
|
||||
Vue.use(JsonSchemaEditor);
|
||||
|
|
Loading…
Reference in New Issue