mxd před 3 roky
rodič
revize
49b627fcd7

+ 18 - 0
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicGroupController.java

@@ -70,4 +70,22 @@ public class MagicGroupController extends MagicController implements MagicExcept
 		isTrue(allowVisit(request, Authorization.SAVE, group), PERMISSION_INVALID);
 		return new JsonBean<>(magicAPIService.createGroup(group));
 	}
+
+	/**
+	 * 复制分组
+	 */
+	@RequestMapping("/group/copy")
+	@ResponseBody
+	@Valid(readonly = false)
+	public JsonBean<String> copyGroup(HttpServletRequest request, String src, String target) {
+		Group group = magicAPIService.getGroup(src);
+		notNull(group, GROUP_NOT_FOUND);
+		if(!"0".equals(target)){
+			Group targetGroup = magicAPIService.getGroup(target);
+			notNull(targetGroup, GROUP_NOT_FOUND);
+			isTrue(allowVisit(request, Authorization.SAVE, targetGroup), PERMISSION_INVALID);
+		}
+		isTrue(allowVisit(request, Authorization.VIEW, group), PERMISSION_INVALID);
+		return new JsonBean<>(magicAPIService.copyGroup(src, target));
+	}
 }

+ 2 - 0
magic-api/src/main/java/org/ssssssss/magicapi/model/ApiInfo.java

@@ -312,6 +312,8 @@ public class ApiInfo extends MagicEntity {
 		info.setPaths(this.paths);
 		info.setRequestBodyDefinition(this.requestBodyDefinition);
 		info.setResponseBodyDefinition(this.responseBodyDefinition);
+		info.setLock(this.lock);
+		info.setProperties(this.properties);
 		return info;
 	}
 }

+ 16 - 0
magic-api/src/main/java/org/ssssssss/magicapi/model/FunctionInfo.java

@@ -87,4 +87,20 @@ public class FunctionInfo extends MagicEntity {
 	public int hashCode() {
 		return Objects.hash(id, path, script, name, groupId, parameters, description, returnType);
 	}
+
+	public FunctionInfo copy(){
+		FunctionInfo info = new FunctionInfo();
+		info.setId(this.id);
+		info.setName(this.name);
+		info.setGroupId(this.groupId);
+		info.setScript(this.script);
+		info.setDescription(this.description);
+		info.setParameters(this.parameters);
+		info.setPath(this.path);
+		info.setMappingPath(this.mappingPath);
+		info.setReturnType(this.returnType);
+		info.setProperties(this.properties);
+		info.setLock(this.lock);
+		return info;
+	}
 }

+ 2 - 0
magic-api/src/main/java/org/ssssssss/magicapi/provider/MagicAPIService.java

@@ -209,4 +209,6 @@ public interface MagicAPIService extends MagicModule {
 	 * 处理刷新通知
 	 */
 	boolean processNotify(MagicNotify magicNotify);
+
+	String copyGroup(String src, String target);
 }

+ 35 - 9
magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/DefaultMagicAPIService.java

@@ -123,7 +123,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 						MagicScriptContext.set(newContext);
 						try {
 							Object value = ScriptManager.executeScript(info.getScript(), newContext);
-							if(value instanceof ExitValue){
+							if (value instanceof ExitValue) {
 								throw new MagicExitException((ExitValue) value);
 							}
 							return value;
@@ -248,8 +248,8 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		return lockWithNotify(functionServiceProvider.unlock(id), id, NOTIFY_ACTION_FUNCTION);
 	}
 
-	private boolean lockWithNotify(boolean success, String id, int type){
-		if(success){
+	private boolean lockWithNotify(boolean success, String id, int type) {
+		if (success) {
 			magicNotifyService.sendNotify(new MagicNotify(instanceId, id, NOTIFY_ACTION_UPDATE, type));
 		}
 		return success;
@@ -315,7 +315,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 			isTrue(!functionServiceProvider.existsWithoutId(functionInfo), FUNCTION_ALREADY_EXISTS.format(functionInfo.getPath()));
 			FunctionInfo oldInfo = functionServiceProvider.get(functionInfo.getId());
 			isTrue(functionServiceProvider.update(functionInfo), FUNCTION_SAVE_FAILURE);
-			if(!oldInfo.getScript().equals(functionInfo.getScript())){
+			if (!oldInfo.getScript().equals(functionInfo.getScript())) {
 				backupService.backup(functionInfo);
 			}
 		}
@@ -582,7 +582,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 			// 检查上级分组是否存在
 			isTrue("0".equals(group.getParentId()) || groupServiceProvider.getGroupResource(group.getParentId()).exists(), GROUP_NOT_FOUND);
 		}
-		if(checked) {
+		if (checked) {
 			// 检测分组是否有冲突
 			groups.forEach(group -> {
 				Resource resource;
@@ -596,7 +596,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 					isTrue(src == null || src.getId().equals(group.getId()), GROUP_CONFLICT);
 				}
 			});
-		}else{
+		} else {
 			Resource resource = workspace.getDirectory(PATH_API);
 			resource.delete();
 			resource.mkdir();
@@ -737,7 +737,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 			case NOTIFY_ACTION_ALL:
 				return processAllNotify();
 		}
-		switch (action){
+		switch (action) {
 			case NOTIFY_WS_C_S:
 				return processWebSocketMessageReceived(magicNotify.getSessionId(), magicNotify.getContent());
 			case NOTIFY_WS_S_C:
@@ -746,6 +746,32 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		return false;
 	}
 
+	@Override
+	public String copyGroup(String srcId, String target) {
+		Group src = getGroup(srcId);
+		src.setId(null);
+		src.setParentId(target);
+		src.setName(src.getName() + "(复制)");
+		src.setPath(src.getPath() + "_copy");
+		String newId = createGroup(src);
+		if (GROUP_TYPE_API.equals(src.getType())) {
+			apiServiceProvider.listWithScript()
+					.stream().filter(it -> srcId.equals(it.getGroupId()))
+					.map(ApiInfo::copy)
+					.peek(it -> it.setGroupId(newId))
+					.peek(it -> it.setId(null))
+					.forEach(this::saveApi);
+		} else {
+			functionServiceProvider.listWithScript()
+					.stream().filter(it -> srcId.equals(it.getGroupId()))
+					.map(FunctionInfo::copy)
+					.peek(it -> it.setGroupId(newId))
+					.peek(it -> it.setId(null))
+					.forEach(this::saveFunction);
+		}
+		return newId;
+	}
+
 	@Override
 	public String getModuleName() {
 		return "magic";
@@ -894,7 +920,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 			for (Resource file : root.files(".ms")) {
 				if (isApi) {
 					ApiInfo info = apiServiceProvider.deserialize(file.read());
-					if (checked){
+					if (checked) {
 						checkApiConflict(info);
 					}
 					apiInfos.add(info);
@@ -902,7 +928,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 					isTrue(apiPaths.add(apiPath), UPLOAD_PATH_CONFLICT.format(apiPath));
 				} else {
 					FunctionInfo info = functionServiceProvider.deserialize(file.read());
-					if (checked){
+					if (checked) {
 						checkFunctionConflict(info);
 					}
 					functionInfos.add(info);

+ 32 - 1
magic-editor/src/console/src/components/resources/magic-api-list.vue

@@ -83,6 +83,15 @@
         <button class="ma-button" @click="createGroupAction(false)">取消</button>
       </template>
     </magic-dialog>
+    <magic-dialog v-model="groupChooseVisible" title="复制分组" align="right" :moveable="false" width="340px" height="390px"
+                  className="ma-tree-wrapper">
+      <template #content>
+        <magic-group-choose ref="groupChoose" rootName="接口分组" type="1" height="300px" max-height="300px"/>
+      </template>
+      <template #buttons>
+        <button class="ma-button active" @click="copyGroup">复制</button>
+      </template>
+    </magic-dialog>
   </div>
 </template>
 
@@ -92,6 +101,7 @@ import MagicTree from '../common/magic-tree.vue'
 import request from '@/api/request.js'
 import MagicDialog from '@/components/common/modal/magic-dialog.vue'
 import MagicInput from '@/components/common/magic-input.vue'
+import MagicGroupChoose from '@/components/resources/magic-group-choose.vue'
 import { replaceURL, download as downloadFile, requestGroup, goToAnchor, deepClone } from '@/scripts/utils.js'
 import contants from '@/scripts/contants.js'
 import Key from '@/scripts/hotkey.js'
@@ -104,7 +114,8 @@ export default {
   components: {
     MagicTree,
     MagicDialog,
-    MagicInput
+    MagicInput,
+    MagicGroupChoose
   },
   data() {
     return {
@@ -117,6 +128,8 @@ export default {
       tree: [],
       // 数据排序规则,true:升序,false:降序
       treeSort: true,
+      groupChooseVisible: false,
+      srcId: '',
       // 新建分组对象
       createGroupObj: {
         visible: false,
@@ -358,6 +371,15 @@ export default {
               this.openCreateGroupModal(item)
             }
           },
+          {
+            label: '复制分组',
+            icon: 'ma-icon-copy',
+            onClick: () => {
+              this.srcId = item.id
+              this.groupChooseVisible = true
+              this.$refs.groupChoose.initData()
+            }
+          },
           {
             label: '删除分组',
             icon: 'ma-icon-delete',
@@ -493,6 +515,15 @@ export default {
       })
       return false
     },
+    copyGroup(){
+      let target = this.$refs.groupChoose.getSelected()
+      if(target && this.srcId){
+        this.groupChooseVisible = false
+        request.send('group/copy', { src: this.srcId, target }).success(() => {
+          this.initData();
+        })
+      }
+    },
     // 删除接口
     deleteApiInfo(item) {
       bus.$emit('status', `准备删除接口「${item.name}(${item.path})」`)

+ 33 - 1
magic-editor/src/console/src/components/resources/magic-function-list.vue

@@ -81,6 +81,16 @@
         <button class="ma-button" @click="createGroupAction(false)">取消</button>
       </template>
     </magic-dialog>
+
+    <magic-dialog v-model="groupChooseVisible" title="复制分组" align="right" :moveable="false" width="340px" height="390px"
+                  className="ma-tree-wrapper">
+      <template #content>
+        <magic-group-choose ref="groupChoose" rootName="函数分组" type="2" height="300px" max-height="300px"/>
+      </template>
+      <template #buttons>
+        <button class="ma-button active" @click="copyGroup">复制</button>
+      </template>
+    </magic-dialog>
   </div>
 </template>
 
@@ -90,6 +100,7 @@ import MagicTree from '@/components/common/magic-tree.vue'
 import request from '@/api/request.js'
 import MagicDialog from '@/components/common/modal/magic-dialog.vue'
 import MagicInput from '@/components/common/magic-input.vue'
+import MagicGroupChoose from '@/components/resources/magic-group-choose.vue'
 import { replaceURL, requestGroup, goToAnchor, deepClone } from '@/scripts/utils.js'
 import JavaClass from '@/scripts/editor/java-class.js'
 import Key from '@/scripts/hotkey.js'
@@ -103,7 +114,8 @@ export default {
   components: {
     MagicTree,
     MagicDialog,
-    MagicInput
+    MagicInput,
+    MagicGroupChoose
   },
   data() {
     return {
@@ -116,6 +128,8 @@ export default {
       tree: [],
       // 数据排序规则,true:升序,false:降序
       treeSort: true,
+      groupChooseVisible: false,
+      srcId: '',
       // 新建分组对象
       createGroupObj: {
         visible: false,
@@ -354,6 +368,15 @@ export default {
               this.openCreateGroupModal(item)
             }
           },
+          {
+            label: '复制分组',
+            icon: 'ma-icon-copy',
+            onClick: () => {
+              this.srcId = item.id
+              this.groupChooseVisible = true
+              this.$refs.groupChoose.initData()
+            }
+          },
           {
             label: '删除分组',
             icon: 'ma-icon-delete',
@@ -457,6 +480,15 @@ export default {
       })
       return false
     },
+    copyGroup(){
+      let target = this.$refs.groupChoose.getSelected()
+      if(target && this.srcId){
+        this.groupChooseVisible = false
+        request.send('group/copy', { src: this.srcId, target }).success(() => {
+          this.initData();
+        })
+      }
+    },
     // 删除接口
     deleteApiInfo(item) {
       bus.$emit('status', `准备删除函数「${item.name}(${item.path})」`)

+ 199 - 0
magic-editor/src/console/src/components/resources/magic-group-choose.vue

@@ -0,0 +1,199 @@
+<template>
+    <magic-tree :data="tree" :forceUpdate="forceUpdate" :style="{ height, maxHeight}" style="overflow: auto" :loading="showLoading > 0">
+      <template #folder="{ item }">
+        <div
+            :style="{ 'padding-left': 17 * item.level + 'px' }"
+            :title="`${item.name||''}${item.parentId !== 'root' ? '(' + (item.path || '') + ')' : ''}`"
+            class="ma-tree-item-header ma-tree-hover"
+            @click.stop="$set(item,'opened',!item.opened)"
+        >
+          <magic-checkbox :value="item.id === selectedItem" :checked-half="item.checkedHalf" @change="doSelected(item)"/>
+          <i :class="item.opened ? 'ma-icon-arrow-bottom' : 'ma-icon-arrow-right'" class="ma-icon" />
+          <i class="ma-icon ma-icon-list"></i>
+          <label>{{ item.name }}</label>
+          <span v-if="item.parentId !== 'root'">({{ item.path }})</span>
+        </div>
+      </template>
+    </magic-tree>
+</template>
+
+<script>
+import bus from '../../scripts/bus.js'
+import MagicTree from '../common/magic-tree.vue'
+import request from '@/api/request.js'
+import contants from '@/scripts/contants.js'
+import MagicCheckbox from "@/components/common/magic-checkbox";
+
+export default {
+  name: 'MagicGroupChoose',
+  props: {
+    height: {
+      type: String,
+      required: true
+    },
+    maxHeight: {
+      type: String,
+      required: true
+    },
+    rootName: {
+      type: String,
+      required: true
+    },
+    type: {
+      type: String,
+      required: true
+    }
+  },
+  components: {
+    MagicCheckbox,
+    MagicTree
+  },
+  data() {
+    return {
+      selectedItem: '',
+      bus: bus,
+      // 分组list数据
+      listGroupData: [],
+      // 分组+接口tree数据
+      tree: [],
+      // 数据排序规则,true:升序,false:降序
+      treeSort: true,
+      // 绑定给magic-tree组件,用来触发子组件强制更新
+      forceUpdate: true,
+      // 是否展示tree-loading,0表示不展示,大于0表示展示
+      showLoading: 0
+    }
+  },
+  methods: {
+    // 初始化数据
+    initData() {
+      this.showLoading = 1
+      this.tree = []
+      this.listGroupData = [
+          { id: '0',_type: 'root', name: this.rootName, parentId: 'root', path:'', selected: false, checkedHalf: false}
+      ]
+      request.send(`group/list?type=${this.type}`).success(data => {
+        data = data || []
+        this.listGroupData.push(...data.map(it => {
+          it.selected = false;
+          it.checkedHalf = false;
+          it._type = 'group';
+          return it;
+        }))
+        this.initTreeData()
+        this.showLoading--
+      })
+    },
+    // 初始化tree结构数据
+    initTreeData() {
+      // 1.把所有的分组id存map,方便接口列表放入,root为没有分组的接口
+      let groupItem = {root: []}
+      this.listGroupData.forEach(element => {
+        groupItem[element.id] = []
+        element.folder = true
+        this.$set(element, 'opened', true)
+        // 缓存一个name和path给后面使用
+        element.tmpName = element.name.indexOf('/') === 0 ? element.name : '/' + element.name
+        element.tmpPath = element.path.indexOf('/') === 0 ? element.path : '/' + element.path
+      })
+      // 3.将分组列表变成tree,并放入接口列表,分组在前,接口在后
+      let arrayToTree = (arr, parentItem, groupName, groupPath, level) => {
+        //  arr 是返回的数据  parendId 父id
+        let temp = []
+        let treeArr = arr
+        treeArr.forEach((item, index) => {
+          if (item.parentId === parentItem.id) {
+            item.level = level
+            item.tmpName = groupName + item.tmpName
+            item.tmpPath = groupPath + item.tmpPath
+            // 递归调用此函数
+            item.children = arrayToTree(treeArr, item, item.tmpName, item.tmpPath, level + 1)
+            if (groupItem[item.id]) {
+              groupItem[item.id].forEach(element => {
+                element.level = item.level + 1
+                element.groupName = item.tmpName
+                element.groupPath = item.tmpPath
+                element.groupId = item.id
+                this.$set(item.children, item.children.length, element)
+              })
+            }
+            this.$set(temp, temp.length, treeArr[index])
+          }
+        })
+        return temp
+      }
+      this.tree = [...arrayToTree(this.listGroupData, {id: 'root'}, '', '', 0), ...groupItem['root']]
+      this.sortTree()
+    },
+    getSelected() {
+      return this.selectedItem
+    },
+    doSelected(item) {
+      this.selectedItem = item.id
+    },
+    // 排序tree,分组在前,接口在后
+    sortTree() {
+      if (this.treeSort === null) {
+        return
+      }
+      let sortItem = function (item1, item2) {
+        return item1.name.localeCompare(item2.name, 'zh-CN')
+      }
+      let sortHandle = arr => {
+        // 分组
+        let folderArr = []
+        // 接口
+        let fileArr = []
+        arr.forEach(element => {
+          if (element.folder === true) {
+            if (element.children && element.children.length > 0) {
+              element.children = sortHandle(element.children)
+            }
+            folderArr.push(element)
+          } else {
+            fileArr.push(element)
+          }
+        })
+        folderArr.sort(sortItem)
+        fileArr.sort(sortItem)
+        if (this.treeSort === false) {
+          folderArr.reverse()
+          fileArr.reverse()
+        }
+        return folderArr.concat(fileArr)
+      }
+      this.tree = sortHandle(this.tree)
+      this.changeForceUpdate()
+    },
+    // 强制触发子组件更新
+    changeForceUpdate() {
+      this.forceUpdate = !this.forceUpdate
+    }
+  }
+}
+</script>
+
+<style>
+@import './magic-resource.css';
+.ma-tree-wrapper .ma-checkbox input + label{
+  width: 12px !important;
+  height: 12px !important;
+}
+.ma-tree-wrapper .ma-checkbox input + label::after{
+  width: 12px !important;
+  height: 12px !important;
+  line-height: 12px !important;
+  top: 0 !important;
+  left: 0 !important;
+}
+</style>
+<style scoped>
+.ma-checkbox{
+  display: inline-block;
+  width: 20px;
+  height: 12px;
+}
+.ma-tree-wrapper .ma-tree-container{
+  height: 100%;
+}
+</style>