feat(功能用例): 脑图保存

This commit is contained in:
baiqi 2024-06-03 15:01:57 +08:00 committed by 刘瑞斌
parent d258f4e669
commit 0e306a8ac2
10 changed files with 117 additions and 75 deletions

View File

@ -1,6 +1,6 @@
<template>
<div class="h-full pl-[16px]">
<div class="baseInfo-form">
<div class="baseInfo-form" :class="props.activeCase.isNew ? 'baseInfo-form--no-bottom' : ''">
<a-skeleton v-if="baseInfoLoading || props.loading" :loading="baseInfoLoading || props.loading" :animation="true">
<a-space direction="vertical" class="w-full" size="large">
<a-skeleton-line :rows="10" :line-height="30" :line-spacing="30" />
@ -27,7 +27,7 @@
</a-form-item>
</a-form>
</div>
<div class="flex items-center gap-[12px] bg-white py-[16px]">
<div v-if="!props.activeCase.isNew" class="flex items-center gap-[12px] bg-white py-[16px]">
<a-button
v-permission="['FUNCTIONAL_CASE:READ+UPDATE']"
type="primary"
@ -200,4 +200,7 @@
overflow-y: auto;
height: calc(100% - 64px);
}
.baseInfo-form--no-bottom {
height: 100%;
}
</style>

View File

@ -40,6 +40,8 @@
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue';
import { FormItem } from '@/components/pure/ms-form-create/types';
import MsMinderEditor from '@/components/pure/ms-minder-editor/minderEditor.vue';
import type { MinderJson, MinderJsonNode, MinderJsonNodeData } from '@/components/pure/ms-minder-editor/props';
@ -113,7 +115,7 @@
loading.value = true;
const res = await getCaseModuleTree({
projectId: appStore.currentProjectId,
moduleId: props.moduleId === 'all' ? '' : props.moduleId,
moduleId: props.moduleId === 'NONE' ? '' : props.moduleId,
});
caseTree.value = mapTree<MinderJsonNode>(res, (e) => ({
...e,
@ -142,7 +144,7 @@
importJson.value.root = {
children: caseTree.value,
data: {
id: 'all',
id: 'NONE',
text: t('ms.minders.allModule'),
resource: [moduleTag],
},
@ -204,10 +206,10 @@
* @param node 用例节点
*/
function getCaseNodeInfo(node: MinderJsonNode) {
let textStep: MinderJsonNode | undefined;
let prerequisiteNode: MinderJsonNode | undefined;
let remarkNode: MinderJsonNode | undefined;
const stepNodes: MinderJsonNode[] = [];
let textStep: MinderJsonNode | undefined; //
let prerequisiteNode: MinderJsonNode | undefined; //
let remarkNode: MinderJsonNode | undefined; //
const stepNodes: MinderJsonNode[] = []; //
node.children?.forEach((item) => {
if (item.data.resource?.includes(textTag)) {
textStep = item;
@ -230,7 +232,7 @@
return {
prerequisite: prerequisiteNode?.data.text || '',
caseEditType: steps.length > 0 ? 'STEP' : ('TEXT' as FeatureCaseMinderEditType),
steps,
steps: JSON.stringify(steps),
textDescription: textStep?.data.text || '',
expectedResult: textStep?.children?.[0]?.data.text || '',
description: remarkNode?.data.text || '',
@ -242,25 +244,29 @@
*/
function makeMinderParams(): FeatureCaseMinderUpdateParams {
const fullJson: MinderJson = window.minder.exportJson();
filterTree(fullJson.root.children, (node) => {
filterTree(fullJson.root.children, (node, parent) => {
if (node.data.isNew !== false || node.data.changed === true) {
if (node.data.resource?.includes(moduleTag)) {
tempMinderParams.value.updateModuleList.push({
id: node.data.id,
name: node.data.text,
parentId: node.parent?.data.id || '',
type: node.data.isNew ? 'ADD' : 'UPDATE',
parentId: parent?.data.id || 'NONE',
type: node.data.isNew !== false ? 'ADD' : 'UPDATE',
moveMode: node.data.moveMode,
targetId: node.data.targetId,
});
} else if (node.data.resource?.includes(caseTag)) {
const caseNodeInfo = getCaseNodeInfo(node as MinderJsonNode);
tempMinderParams.value.updateCaseList.push({
id: node.data.id,
name: node.data.text,
moduleId: node.parent?.data.id || '',
type: node.data.isNew ? 'ADD' : 'UPDATE',
moduleId: parent?.data.id || '',
type: node.data.isNew !== false ? 'ADD' : 'UPDATE',
templateId: templateId.value,
tags: node.data.resource || [],
customFields: baseInfoRef.value?.makeParams().customFields || [],
moveMode: node.data.moveMode,
targetId: node.data.targetId,
...caseNodeInfo,
});
return false; //
@ -273,10 +279,14 @@
async function handleMinderSave() {
try {
loading.value = true;
await saveCaseMinder(makeMinderParams());
Message.success(t('common.saveSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
@ -284,9 +294,24 @@
* 已选中节点的可替换标签判断
* @param node 选中节点
*/
function replaceableTags(node: MinderJsonNode) {
if (Object.keys(node.data || {}).length === 0 || node.data?.id === 'root') {
//
function replaceableTags(node: MinderJsonNode, nodes: MinderJsonNode[]) {
if (nodes.length > 1) {
// 1
if (nodes.some((e) => (e.data?.resource || []).length > 0)) {
//
return [];
}
if (nodes.every((e) => (e.data?.resource || []).length === 0)) {
//
return [moduleTag];
}
}
if (
Object.keys(node.data || {}).length === 0 ||
node.data?.id === 'root' ||
(node.parent?.data.resource || []).length === 0
) {
//
return [];
}
if (node.data?.resource?.some((e) => topTags.includes(e))) {
@ -334,6 +359,22 @@
function execInert(command: string, node?: MinderJsonNodeData) {
if (window.minder.queryCommandState(command) !== -1) {
window.minder.execCommand(command, node);
nextTick(() => {
const newNode: MinderJsonNode = window.minder.getSelectedNode();
newNode.data.isNew = true; //
switch (command) {
case 'AppendChildNode':
newNode.data.moveMode = 'APPEND'; //
newNode.data.targetId = newNode.parent?.data.id || '';
break;
case 'AppendSiblingNode':
newNode.data.moveMode = 'AFTER'; //
newNode.data.targetId = newNode.data.id || '';
break;
default:
break;
}
});
}
}
@ -476,6 +517,9 @@
} else if (node.data?.resource?.includes(prerequisiteTag) && (!node.children || node.children.length === 0)) {
//
execInert('AppendChildNode');
} else {
//
execInert('AppendChildNode');
}
break;
case 'AppendSiblingNode':
@ -535,33 +579,12 @@
tempMinderParams.value.updateCaseList = tempMinderParams.value.updateCaseList.filter(
(e) => e.id !== node.data.id
);
// tempMinderParams.value.updateModuleList.push({
// id: node.data.id,
// name: node.data.text,
// type: 'ADD',
// parentId: node.parent?.data.id || '',
// });
window.minder.execCommand('priority');
} else if (node.data.resource?.includes(caseTag)) {
//
tempMinderParams.value.updateModuleList = tempMinderParams.value.updateModuleList.filter(
(e) => e.id !== node.data.id
);
// tempMinderParams.value.updateCaseList.push({
// id: node.data.id,
// name: node.data.text,
// moduleId: node.parent?.data.id || '',
// type: 'ADD',
// templateId: templateId.value,
// prerequisite: '',
// caseEditType: 'STEP',
// steps: [],
// textDescription: '',
// expectedResult: '',
// description: '',
// tags: [],
// customFields: [],
// });
}
}
const baseInfoLoading = ref(false);
@ -589,7 +612,7 @@
label: t('caseManagement.featureCase.bug'),
},
];
if (activeCase.value.id) {
if (!activeCase.value.isNew) {
return fullTabList;
}
return fullTabList.filter((item) => item.value === 'baseInfo');
@ -629,7 +652,6 @@
if (fileIds.length) {
checkUpdateFileIds.value = await checkFileIsUpdateRequest(fileIds);
}
formRules.value = initFormCreate(res.customFields, ['FUNCTIONAL_CASE:READ+UPDATE']);
if (res.attachments) {
//
fileList.value = res.attachments
@ -644,6 +666,7 @@
return convertToFile(fileInfo);
});
}
formRules.value = initFormCreate(res.customFields, ['FUNCTIONAL_CASE:READ+UPDATE']);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -663,20 +686,22 @@
}
function handleContentChange(node: MinderJsonNode) {
const { resource } = node.data;
//
if (
resource?.includes(prerequisiteTag) ||
resource?.includes(stepTag) ||
resource?.includes(textTag) ||
resource?.includes(remarkTag)
) {
if (node.parent) {
node.parent.data.changed = true;
if (node?.data) {
const { resource } = node.data;
//
if (
resource?.includes(prerequisiteTag) ||
resource?.includes(stepTag) ||
resource?.includes(textTag) ||
resource?.includes(remarkTag)
) {
if (node.parent?.data) {
node.parent.data.changed = true;
}
} else if (node.parent?.parent?.data?.resource?.includes(caseTag)) {
//
node.parent.parent.data.changed = true;
}
} else if (node.parent?.parent?.data.resource?.includes(caseTag)) {
//
node.parent.parent.data.changed = true;
}
}
@ -690,7 +715,16 @@
extraVisible.value = true;
activeExtraKey.value = 'baseInfo';
resetExtractInfo();
initCaseDetail(data);
if (data.isNew === false) {
//
initCaseDetail(data);
} else {
activeCase.value = {
id: data.id,
name: data.text,
isNew: true,
};
}
} else if (data?.resource?.includes(moduleTag) && data.count > 0 && data.isLoaded !== true) {
try {
loading.value = true;

View File

@ -266,6 +266,10 @@
}
if (window.minder.queryCommandState(command) !== -1) {
window.minder.execCommand(command);
nextTick(() => {
const newNode: MinderJsonNode = window.minder.getSelectedNode();
newNode.data.isNew = true; //
});
}
}

View File

@ -46,11 +46,12 @@
minder = window.minder;
minder.on('selectionchange', () => {
commandDisabled.value = isDisable();
const nodes: MinderJsonNode[] = window.minder.getSelectedNodes();
const node: MinderJsonNode = minder.getSelectedNode();
if (commandDisabled.value) {
tagList.value = [];
} else if (props.replaceableTags) {
tagList.value = props.replaceableTags(node);
tagList.value = props.replaceableTags(node, nodes);
} else {
tagList.value = [];
}
@ -98,10 +99,11 @@
}
}
window.minder.execCommand('resource', origin);
const nodes: MinderJsonNode[] = window.minder.getSelectedNodes();
const node: MinderJsonNode = minder.getSelectedNode();
minderStore.dispatchEvent(MinderEventName.SET_TAG, undefined, undefined, node);
if (props.replaceableTags) {
tagList.value = props.replaceableTags(node);
tagList.value = props.replaceableTags(node, nodes);
}
if (props.afterTagEdit) {
props.afterTagEdit(node, resourceName);

View File

@ -1,5 +1,5 @@
<template>
<a-spin :loading="loading" :tip="t('minder.loading')" class="ms-minder-editor-container">
<a-spin :loading="loading" class="ms-minder-editor-container">
<div class="flex-1">
<minderHeader
:sequence-enable="props.sequenceEnable"
@ -67,7 +67,6 @@
import minderHeader from './main/header.vue';
import mainEditor from './main/mainEditor.vue';
import { useI18n } from '@/hooks/useI18n';
import { MinderEvent } from '@/store/modules/components/minder-editor/types';
import useEventListener from './hooks/useEventListener';
@ -106,8 +105,6 @@
...viewMenuProps,
});
const { t } = useI18n();
const loading = defineModel<boolean>('loading', {
default: false,
});

View File

@ -2,6 +2,8 @@
* Api
*/
import type { MoveMode } from '@/models/common';
import type { PropType } from 'vue';
export interface MinderIconButtonItem {
@ -18,6 +20,8 @@ export interface MinderJsonNodeData {
// 前端渲染字段
isNew?: boolean; // 是否脑图新增节点,需要在初始化脑图数据时标记已存在节点为 false 以区分是否新增节点
changed?: boolean; // 脑图节点是否发生过变化
moveMode?: MoveMode; // 移动方式(节点移动或新增时需要)
targetId?: string; // 目标节点 id节点移动或新增时需要
[key: string]: any;
}
export interface MinderJsonNode {
@ -105,7 +109,7 @@ export const tagProps = {
type: Boolean,
default: false,
},
replaceableTags: Function as PropType<(node: MinderJsonNode) => string[]>,
replaceableTags: Function as PropType<(node: MinderJsonNode, nodes: MinderJsonNode[]) => string[]>,
tagDisableCheck: Function,
tagEditCheck: Function as PropType<(node: MinderJsonNode, tag: string) => boolean>,
afterTagEdit: Function as PropType<(node: MinderJsonNode, tag: string) => void>,

View File

@ -405,7 +405,7 @@ export interface FeatureCaseMinderUpdateCaseItem {
targetId?: string;
prerequisite: string; // 前置条件
caseEditType: FeatureCaseMinderEditType;
steps: FeatureCaseMinderStepItem[];
steps: string;
textDescription: string; // 文本描述
expectedResult: string; // 期望结果
description: string;

View File

@ -281,8 +281,9 @@ export function mapTree<T>(
*/
export function filterTree<T>(
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
filterFn: (node: TreeNode<T>) => boolean,
customChildrenKey = 'children'
filterFn: (node: TreeNode<T>, parent?: TreeNode<T> | null) => boolean,
customChildrenKey = 'children',
parentNode: TreeNode<T> | null = null
): TreeNode<T>[] {
if (!Array.isArray(tree)) {
tree = [tree];
@ -291,11 +292,11 @@ export function filterTree<T>(
for (let i = 0; i < tree.length; i++) {
const node = (tree as TreeNode<T>[])[i];
// 如果节点满足过滤条件,则保留该节点,并递归过滤子节点
if (filterFn(node)) {
if (filterFn(node, parentNode)) {
const newNode = cloneDeep(node);
if (node[customChildrenKey] && node[customChildrenKey].length > 0) {
// 递归过滤子节点,并将过滤后的子节点添加到当前节点中
newNode[customChildrenKey] = filterTree(node[customChildrenKey], filterFn, customChildrenKey);
newNode[customChildrenKey] = filterTree(node[customChildrenKey], filterFn, customChildrenKey, node);
} else {
newNode[customChildrenKey] = [];
}

View File

@ -199,9 +199,6 @@
<MsIcon :size="14" type="icon-icon_mindnote_outlined" />
</a-radio>
</a-radio-group>
<MsTag no-margin size="large" class="cursor-pointer" theme="outline" @click="fetchData">
<MsIcon class="text-[16px] text-[var(color-text-4)]" :size="32" type="icon-icon_reset_outlined" />
</MsTag>
</div>
</div>
<div class="mt-[16px] h-[calc(100%-32px)] border-t border-[var(--color-text-n8)]">

View File

@ -271,15 +271,15 @@
type="line"
@change="(v: boolean | string| number) => handleMenuStatusChange('BUG_SYNC_SYNC_ENABLE',v as boolean, MenuEnum.bugManagement)"
/>
<!-- 功能测试 同步缺陷 -->
<div v-permission="['PROJECT_APPLICATION_BUG:UPDATE']">
<!-- 测试用例 关联需求 -->
<div v-permission="['PROJECT_APPLICATION_CASE:UPDATE']">
<a-tooltip v-if="record.type === 'CASE_RELATED' && !allValueMap['CASE_RELATED_CASE_ENABLE']" position="tr">
<template #content>
<span>
{{ t('project.menu.notConfig') }}
<span class="cursor-pointer text-[rgb(var(--primary-4))]" @click="showDefectDrawer">{{
t(`project.menu.${record.type}`)
}}</span>
<span class="cursor-pointer text-[rgb(var(--primary-4))]" @click="showDefectDrawer">
{{ t(`project.menu.${record.type}`) }}
</span>
{{ t('project.menu.configure') }}
</span>
</template>
@ -383,7 +383,7 @@
<RelatedCase
v-model:visible="relatedCaseDrawerVisible"
@cancel="relatedCaseDrawerVisible = false"
@ok="initMenuData()"
@ok="getMenuConfig(MenuEnum.caseManagement)"
/>
</template>