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

View File

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

View File

@ -266,6 +266,10 @@
} }
if (window.minder.queryCommandState(command) !== -1) { if (window.minder.queryCommandState(command) !== -1) {
window.minder.execCommand(command); 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 = window.minder;
minder.on('selectionchange', () => { minder.on('selectionchange', () => {
commandDisabled.value = isDisable(); commandDisabled.value = isDisable();
const nodes: MinderJsonNode[] = window.minder.getSelectedNodes();
const node: MinderJsonNode = minder.getSelectedNode(); const node: MinderJsonNode = minder.getSelectedNode();
if (commandDisabled.value) { if (commandDisabled.value) {
tagList.value = []; tagList.value = [];
} else if (props.replaceableTags) { } else if (props.replaceableTags) {
tagList.value = props.replaceableTags(node); tagList.value = props.replaceableTags(node, nodes);
} else { } else {
tagList.value = []; tagList.value = [];
} }
@ -98,10 +99,11 @@
} }
} }
window.minder.execCommand('resource', origin); window.minder.execCommand('resource', origin);
const nodes: MinderJsonNode[] = window.minder.getSelectedNodes();
const node: MinderJsonNode = minder.getSelectedNode(); const node: MinderJsonNode = minder.getSelectedNode();
minderStore.dispatchEvent(MinderEventName.SET_TAG, undefined, undefined, node); minderStore.dispatchEvent(MinderEventName.SET_TAG, undefined, undefined, node);
if (props.replaceableTags) { if (props.replaceableTags) {
tagList.value = props.replaceableTags(node); tagList.value = props.replaceableTags(node, nodes);
} }
if (props.afterTagEdit) { if (props.afterTagEdit) {
props.afterTagEdit(node, resourceName); props.afterTagEdit(node, resourceName);

View File

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

View File

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

View File

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

View File

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

View File

@ -199,9 +199,6 @@
<MsIcon :size="14" type="icon-icon_mindnote_outlined" /> <MsIcon :size="14" type="icon-icon_mindnote_outlined" />
</a-radio> </a-radio>
</a-radio-group> </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> </div>
<div class="mt-[16px] h-[calc(100%-32px)] border-t border-[var(--color-text-n8)]"> <div class="mt-[16px] h-[calc(100%-32px)] border-t border-[var(--color-text-n8)]">

View File

@ -271,15 +271,15 @@
type="line" type="line"
@change="(v: boolean | string| number) => handleMenuStatusChange('BUG_SYNC_SYNC_ENABLE',v as boolean, MenuEnum.bugManagement)" @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"> <a-tooltip v-if="record.type === 'CASE_RELATED' && !allValueMap['CASE_RELATED_CASE_ENABLE']" position="tr">
<template #content> <template #content>
<span> <span>
{{ t('project.menu.notConfig') }} {{ t('project.menu.notConfig') }}
<span class="cursor-pointer text-[rgb(var(--primary-4))]" @click="showDefectDrawer">{{ <span class="cursor-pointer text-[rgb(var(--primary-4))]" @click="showDefectDrawer">
t(`project.menu.${record.type}`) {{ t(`project.menu.${record.type}`) }}
}}</span> </span>
{{ t('project.menu.configure') }} {{ t('project.menu.configure') }}
</span> </span>
</template> </template>
@ -383,7 +383,7 @@
<RelatedCase <RelatedCase
v-model:visible="relatedCaseDrawerVisible" v-model:visible="relatedCaseDrawerVisible"
@cancel="relatedCaseDrawerVisible = false" @cancel="relatedCaseDrawerVisible = false"
@ok="initMenuData()" @ok="getMenuConfig(MenuEnum.caseManagement)"
/> />
</template> </template>