feat(功能用例): 脑图读取

This commit is contained in:
baiqi 2024-05-17 20:25:11 +08:00 committed by Craftsman
parent aba2e91ef9
commit d7a675faf4
23 changed files with 1148 additions and 214 deletions

View File

@ -41,6 +41,7 @@ import {
GetAssociationPublicCasePageUrl,
GetAssociationPublicModuleTreeUrl,
GetCaseListUrl,
GetCaseMinderUrl,
GetCaseModulesCountUrl,
GetCaseModuleTreeUrl,
getChangeHistoryListUrl,
@ -67,6 +68,7 @@ import {
publicAssociatedCaseUrl,
RecoverRecycleCaseListUrl,
RestoreCaseListUrl,
SaveCaseMinderUrl,
TransferFileUrl,
UpdateCaseModuleTreeUrl,
UpdateCaseUrl,
@ -78,11 +80,8 @@ import {
import type { BugListItem } from '@/models/bug-management';
import type {
AssociatedList,
BatchDeleteType,
BatchEditCaseType,
BatchMoveOrCopyType,
CaseManagementTable,
CaseModuleQueryParams,
ChangeHistoryItem,
CreateOrUpdateDemand,
CreateOrUpdateModule,
@ -90,6 +89,7 @@ import type {
DeleteDependencyParams,
DemandItem,
DragCase,
FeatureCaseMinder,
ImportExcelType,
ModulesTreeType,
OperationFile,
@ -98,7 +98,7 @@ import type {
} from '@/models/caseManagement/featureCase';
import type { CommonList, ModuleTreeNode, MoveModules, TableQueryParams } from '@/models/common';
import { ProjectListItem } from '@/models/setting/project';
import { AssociateFunctionalCaseItem, TestPlanItem } from '@/models/testPlan/testPlan';
import { AssociateFunctionalCaseItem } from '@/models/testPlan/testPlan';
// 获取模块树
export function getCaseModuleTree(params: TableQueryParams) {
@ -180,6 +180,16 @@ export function batchCopyToModules(data: BatchMoveOrCopyType) {
return MSR.post({ url: `${BatchCopyCaseUrl}`, data });
}
// 保存脑图
export function saveCaseMinder(data: FeatureCaseMinder) {
return MSR.post({ url: `${SaveCaseMinderUrl}`, data });
}
// 获取脑图
export function getCaseMinder(data: { projectId: string; moduleId: string }) {
return MSR.post({ url: `${GetCaseMinderUrl}`, data });
}
// 回收站
// 回收站用例分页表
@ -362,7 +372,7 @@ export function getDrawerDebugPage(data: TableQueryParams) {
return MSR.post<CommonList<CaseManagementTable>>({ url: GetDebugDrawerPageUrl, data });
}
// 关联缺陷
export function associatedDrawerDebug(data: TableQueryParams) {
export function associatedDebug(data: TableQueryParams) {
return MSR.post<CommonList<CaseManagementTable>>({ url: AssociatedDebuggerUrl, data });
}

View File

@ -26,6 +26,8 @@ export const GetCaseCustomFieldsUrl = '/functional/case/default/template/field';
export const GetSearchCustomFieldsUrl = '/functional/case/custom/field';
// 关联文件列表
export const GetAssociatedFilePageUrl = '/attachment/page';
export const SaveCaseMinderUrl = '/functional/mind/case/edit'; // 保存用例脑图
export const GetCaseMinderUrl = '/functional/mind/case/list'; // 获取脑图数据
// 获取模块树
export const GetCaseModuleTreeUrl = '/functional/case/module/tree';

View File

@ -1,8 +1,13 @@
<template>
<a-form-item v-if="props.mode === 'button'" field="attachment" :label="t('caseManagement.featureCase.addAttachment')">
<a-form-item
v-if="props.mode === 'button'"
field="attachment"
:class="props.onlyButton ? 'hidden-item' : ''"
:label="t('caseManagement.featureCase.addAttachment')"
>
<!-- TODO:跟下面统一样式 -->
<div class="flex flex-col">
<div class="mb-1">
<div class="mb-1" :class="props.onlyButton ? 'mb-[12px]' : ''">
<a-dropdown
v-model:popup-visible="buttonDropDownVisible"
:disabled="props.disabled"
@ -41,7 +46,7 @@
</template>
</a-dropdown>
</div>
<div class="!hover:bg-[rgb(var(--primary-1))] !text-[var(--color-text-4)]">
<div v-if="!props.onlyButton" class="!hover:bg-[rgb(var(--primary-1))] !text-[var(--color-text-4)]">
{{ t('system.orgTemplate.addAttachmentTip') }}
</div>
</div>
@ -244,6 +249,7 @@
defineProps<{
disabled?: boolean;
mode?: 'button' | 'input';
onlyButton?: boolean;
accept?: UploadType;
multiple?: boolean;
inputClass?: string;

View File

@ -16,6 +16,7 @@
></a-input>
<div v-else class="flex flex-col justify-between">
<MsRichText
v-if="props.mode === 'rich'"
v-model:raw="currentContent"
v-model:commentIds="commentIds"
:upload-image="props.uploadImage"
@ -23,6 +24,11 @@
class="w-full"
placeholder="ms.comment.enterPlaceHolderTip"
/>
<a-textarea
v-else
v-model:model-value="currentContent"
:placeholder="t('ms.comment.enterPlaceHolderTip')"
></a-textarea>
<div class="mt-4 flex flex-row justify-end gap-[12px]">
<a-button @click="cancelClick">{{ t('common.cancel') }}</a-button>
<a-button type="primary" :disabled="!currentContent" @click="publish">{{ t('common.publish') }}</a-button>
@ -45,12 +51,18 @@
const { t } = useI18n();
const props = defineProps<{
isShowAvatar: boolean; //
isUseBottom: boolean; //
uploadImage?: (file: File) => Promise<any>;
previewUrl?: string;
}>();
const props = withDefaults(
defineProps<{
mode?: 'rich' | 'textarea';
isShowAvatar: boolean; //
isUseBottom: boolean; //
uploadImage?: (file: File) => Promise<any>;
previewUrl?: string;
}>(),
{
mode: 'rich',
}
);
const currentContent = defineModel<string>('defaultValue', { default: '' });
const commentIds = defineModel<string[]>('noticeUserIds', { default: [] });

View File

@ -2,7 +2,7 @@
<MsMinderEditor
v-model:activeExtraKey="activeExtraKey"
:tags="tags"
:import-json="props.importJson"
:import-json="importJson"
:replaceable-tags="replaceableTags"
:insert-node="insertNode"
:priority-disable-check="priorityDisableCheck"
@ -11,54 +11,422 @@
single-tag
tag-enable
sequence-enable
@click="handleNodeClick"
@node-click="handleNodeClick"
@save="handleMinderSave"
>
<template #extractTabContent>
<div>
<div v-if="activeExtraKey === 'baseInfo'" class="pl-[16px]">
<a-skeleton v-if="baseInfoLoading" :loading="baseInfoLoading" :animation="true">
<a-space direction="vertical" class="w-full" size="large">
<a-skeleton-line :rows="rowLength" :line-height="30" :line-spacing="30" />
</a-space>
</a-skeleton>
<a-form v-else ref="baseInfoFormRef" :model="baseInfoForm" layout="vertical">
<a-form-item
field="name"
:label="t('ms.minders.caseName')"
:rules="[{ required: true, message: t('ms.minders.caseNameNotNull') }]"
>
<a-input v-model:model-value="baseInfoForm.name" :placeholder="t('common.pleaseInput')"></a-input>
</a-form-item>
<a-form-item
field="moduleId"
asterisk-position="end"
:label="t('caseManagement.featureCase.ModuleOwned')"
:rules="[{ required: true, message: t('system.orgTemplate.moduleRuleTip') }]"
>
<a-tree-select
v-model="baseInfoForm.moduleId"
:allow-search="true"
:data="caseTree"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
}"
:draggable="false"
:tree-props="{
virtualListProps: {
height: 200,
},
}"
>
<template #tree-slot-title="node">
<a-tooltip :content="`${node.name}`" position="tl">
<div class="one-line-text w-[300px] text-[var(--color-text-1)]">{{ node.name }}</div>
</a-tooltip>
</template>
</a-tree-select>
</a-form-item>
<MsFormCreate
v-if="formRules.length"
ref="formCreateRef"
v-model:api="fApi"
v-model:form-item="formItem"
:form-rule="formRules"
/>
<a-form-item field="tags" :label="t('common.tag')">
<MsTagsInput v-model:model-value="baseInfoForm.tags" :max-tag-count="6" />
</a-form-item>
</a-form>
<div class="flex items-center gap-[12px]">
<a-button type="primary" @click="handleSave">{{ t('common.save') }}</a-button>
<a-button type="secondary">{{ t('common.cancel') }}</a-button>
</div>
</div>
<div v-else-if="activeExtraKey === 'attachment'" class="pl-[16px]">
<MsAddAttachment
v-model:file-list="fileList"
multiple
only-button
@change="handleFileChange"
@link-file="() => (showLinkFileDrawer = true)"
/>
<MsFileList
v-if="fileList.length > 0"
ref="fileListRef"
v-model:file-list="fileList"
mode="static"
:init-file-save-tips="t('ms.upload.waiting_save')"
:show-upload-type-desc="true"
>
<template #actions="{ item }">
<!-- 本地文件 -->
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
<MsButton
v-if="item.status !== 'init' && item.file.type.includes('image/')"
type="button"
status="primary"
class="!mr-[4px]"
@click="handlePreview(item)"
>
{{ t('ms.upload.preview') }}
</MsButton>
<SaveAsFilePopover
v-model:visible="transferVisible"
:saving-file="activeTransferFileParams"
:file-save-as-source-id="activeCase.id"
:file-save-as-api="transferFileRequest"
:file-module-options-api="getTransferFileTree"
source-id-key="caseId"
/>
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="transferFile(item)"
>
{{ t('caseManagement.featureCase.storage') }}
</MsButton>
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
</div>
<!-- 关联文件 -->
<div v-else class="flex flex-nowrap">
<MsButton
v-if="item.file.type.includes('/image')"
type="button"
status="primary"
class="!mr-[4px]"
@click="handlePreview(item)"
>
{{ t('ms.upload.preview') }}
</MsButton>
<MsButton
v-if="activeCase.id"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
<MsButton
v-if="activeCase.id && item.isUpdateFlag"
type="button"
status="primary"
@click="handleUpdateFile(item)"
>
{{ t('common.update') }}
</MsButton>
</div>
</template>
<template #title="{ item }">
<span v-if="item.isUpdateFlag" class="ml-4 flex items-center font-normal text-[rgb(var(--warning-6))]">
<icon-exclamation-circle-fill />
<span>{{ t('caseManagement.featureCase.fileIsUpdated') }}</span>
</span>
</template>
</MsFileList>
</div>
<div v-else-if="activeExtraKey === 'comments'" class="pl-[16px]">
<div class="flex items-center justify-between">
<div class="text-[var(--color-text-4)]">
{{
t('ms.minders.commentTotal', {
num: activeComment === 'caseComment' ? commentList.length : reviewCommentList.length,
})
}}
</div>
<a-select
v-model:model-value="activeComment"
:options="commentTypeOptions"
class="w-[120px]"
@change="getAllCommentList"
></a-select>
</div>
<ReviewCommentList
v-if="activeComment === 'reviewComment' || activeComment === 'executiveComment'"
:review-comment-list="reviewCommentList"
:active-comment="activeComment"
/>
<template v-else>
<MsComment
:upload-image="handleUploadImage"
:comment-list="commentList"
:preview-url="PreviewEditorImageUrl"
@delete="handleDelete"
@update-or-add="handleUpdateOrAdd"
/>
<MsEmpty v-if="commentList.length === 0" />
</template>
<inputComment
ref="commentInputRef"
v-model:content="content"
v-model:notice-user-ids="noticeUserIds"
v-permission="['FUNCTIONAL_CASE:READ+COMMENT']"
:preview-url="PreviewEditorImageUrl"
:is-active="isActive"
mode="textarea"
is-show-avatar
is-use-bottom
:upload-image="handleUploadImage"
@publish="publishHandler"
@cancel="cancelPublish"
/>
</div>
<div v-else class="pl-[16px]">
<a-button
v-if="hasAnyPermission(['FUNCTIONAL_CASE:READ+UPDATE'])"
class="mr-3"
type="primary"
@click="linkBug"
>
{{ t('caseManagement.featureCase.linkDefect') }}
</a-button>
<a-button v-permission="['PROJECT_BUG:READ+ADD']" type="outline" @click="createBug"
>{{ t('caseManagement.featureCase.createDefect') }}
</a-button>
<div class="bug-list">
<div v-for="item of bugList" :key="item.id" class="bug-item">
<div class="mb-[4px] flex items-center justify-between">
<MsButton type="text" @click="goBug(item.id)">{{ item.num }}</MsButton>
<MsButton type="text" @click="disassociateBug(item.id)">
{{ t('ms.add.attachment.cancelAssociate') }}
</MsButton>
</div>
<a-tooltip :content="item.name">
<div class="one-line-text">{{ item.name }}</div>
</a-tooltip>
</div>
<MsEmpty v-if="bugList.length === 0" />
</div>
</div>
</div>
</template>
</MsMinderEditor>
<LinkFileDrawer
v-model:visible="showLinkFileDrawer"
:get-tree-request="getModules"
:get-count-request="getModulesCount"
:get-list-request="getAssociatedFileListUrl"
:get-list-fun-params="getListFunParams"
@save="saveSelectAssociatedFile"
/>
<a-image-preview v-model:visible="previewVisible" :src="imageUrl" />
<AddDefectDrawer
v-if="activeCase.id"
v-model:visible="showCreateBugDrawer"
:case-id="activeCase.id"
:extra-params="{ caseId: activeCase.id }"
@success="initBugList"
/>
<LinkDefectDrawer
v-if="activeCase.id"
v-model:visible="showLinkBugDrawer"
:case-id="activeCase.id"
:drawer-loading="drawerLoading"
@save="saveHandler"
/>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { FormInstance, Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsEmpty from '@/components/pure/ms-empty/index.vue';
import MsFormCreate from '@/components/pure/ms-form-create/ms-form-create.vue';
import { FormItem, FormRuleItem } 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';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
import { MsFileItem } from '@/components/pure/ms-upload/types';
import MsAddAttachment from '@/components/business/ms-add-attachment/index.vue';
import SaveAsFilePopover from '@/components/business/ms-add-attachment/saveAsFilePopover.vue';
import MsComment from '@/components/business/ms-comment/comment';
import inputComment from '@/components/business/ms-comment/input.vue';
import { CommentItem, CommentParams } from '@/components/business/ms-comment/types';
import LinkFileDrawer from '@/components/business/ms-link-file/associatedFileDrawer.vue';
import ReviewCommentList from '@/views/case-management/caseManagementFeature/components/tabContent/tabComment/reviewCommentList.vue';
import {
addOrUpdateCommentList,
associatedDebug,
cancelAssociatedDebug,
createCommentList,
deleteCommentList,
downloadFileRequest,
editorUploadFile,
getAssociatedFileListUrl,
getCaseDefaultFields,
getCaseMinder,
getCaseModuleTree,
getCommentList,
getReviewCommentList,
getTestPlanExecuteCommentList,
getTransferFileTree,
previewFile,
saveCaseMinder,
transferFileRequest,
updateFile,
} from '@/api/modules/case-management/featureCase';
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { getGenerateId } from '@/utils';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import { downloadByteFile, getGenerateId } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import { AssociatedList, OptionsFieldId } from '@/models/caseManagement/featureCase';
import { ModuleTreeNode, TableQueryParams } from '@/models/common';
import { BugManagementRouteEnum } from '@/enums/routeEnum';
import { convertToFile } from '@/views/case-management/caseManagementFeature/components/utils';
const AddDefectDrawer = defineAsyncComponent(
() => import('@/views/case-management/caseManagementFeature/components/tabContent/tabBug/addDefectDrawer.vue')
);
const LinkDefectDrawer = defineAsyncComponent(
() => import('@/views/case-management/caseManagementFeature/components/tabContent/tabBug/linkDefectDrawer.vue')
);
const props = defineProps<{
importJson: MinderJson;
moduleId: string;
moduleName: string;
}>();
const router = useRouter();
const { openModal } = useModal();
const appStore = useAppStore();
const userStore = useUserStore();
const { t } = useI18n();
const importJson = ref<MinderJson>({
root: {},
template: 'default',
treePath: [],
});
async function initMinder() {
try {
const res = await getCaseMinder({
projectId: appStore.currentProjectId,
moduleId: props.moduleId === 'all' ? '' : props.moduleId,
});
importJson.value.root.children = res;
importJson.value.root.data = {
id: props.moduleId === 'all' ? '' : props.moduleId,
text: props.moduleName,
resource: [t('common.module')],
};
window.minder.importJson(importJson.value);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
watchEffect(() => {
if (props.moduleId) {
initMinder();
}
});
const caseTag = t('common.case');
const moduleTag = t('common.module');
const topTags = [moduleTag, caseTag];
const descTags = [t('ms.minders.stepDesc'), t('ms.minders.textDesc')];
const tags = [...topTags, t('ms.minders.precondition'), ...descTags, t('ms.minders.stepExpect'), t('common.remark')];
const visible = ref<boolean>(false);
const nodeData = ref<any>({});
const extractContentTabList = [
{
label: t('common.baseInfo'),
value: 'baseInfo',
},
{
label: t('caseManagement.featureCase.attachment'),
value: 'attachment',
},
{
value: 'comments',
label: t('caseManagement.featureCase.comments'),
},
{
value: 'bug',
label: t('caseManagement.featureCase.bug'),
},
];
const activeExtraKey = ref('baseInfo');
const activeCase = ref<any>({});
const extractContentTabList = computed(() => {
const fullTabList = [
{
label: t('common.baseInfo'),
value: 'baseInfo',
},
{
label: t('caseManagement.featureCase.attachment'),
value: 'attachment',
},
{
value: 'comments',
label: t('caseManagement.featureCase.comments'),
},
{
value: 'bug',
label: t('caseManagement.featureCase.bug'),
},
];
if (activeCase.value.id) {
return fullTabList;
}
return fullTabList.filter((item) => item.value === 'baseInfo');
});
const activeExtraKey = ref<'baseInfo' | 'attachment' | 'comments' | 'bug'>('baseInfo');
function handleNodeClick(data: any) {
if (data.resource && data.resource.includes(caseTag)) {
visible.value = true;
nodeData.value = data;
activeCase.value = data;
}
}
async function handleMinderSave(data: any) {
try {
await saveCaseMinder({
projectId: appStore.currentProjectId,
versionId: '',
updateCaseList: data,
updateModuleList: [],
deleteResourceList: [],
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
@ -75,6 +443,13 @@
}
if (node.data?.resource?.some((e) => descTags.includes(e))) {
//
if (
node.data?.resource?.includes(t('ms.minders.stepDesc')) &&
(node.parent?.children?.filter((e) => e.data?.resource?.includes(t('ms.minders.stepDesc'))) || []).length > 1
) {
//
return [];
}
return descTags.filter((tag) => !node.data?.resource?.includes(tag));
}
if (
@ -328,6 +703,456 @@
window.minder.execCommand('priority');
}
}
const baseInfoFormRef = ref<FormInstance>();
const baseInfoForm = ref({
name: '',
tags: [],
templateId: '',
moduleId: 'root',
});
const baseInfoLoading = ref(false);
const rowLength = ref<number>(0);
const formRules = ref<FormItem[]>([]);
const formItem = ref<FormRuleItem[]>([]);
const fApi = ref<any>(null);
//
async function initDefaultFields() {
formRules.value = [];
try {
baseInfoLoading.value = true;
const res = await getCaseDefaultFields(appStore.currentProjectId);
const { customFields, id } = res;
baseInfoForm.value.templateId = id;
const result = customFields.map((item: any) => {
const memberType = ['MEMBER', 'MULTIPLE_MEMBER'];
let initValue = item.defaultValue;
const optionsValue: OptionsFieldId[] = item.options;
if (memberType.includes(item.type)) {
if (item.defaultValue === 'CREATE_USER' || item.defaultValue.includes('CREATE_USER')) {
initValue = item.type === 'MEMBER' ? userStore.id : [userStore.id];
}
}
return {
type: item.type,
name: item.fieldId,
label: item.fieldName,
value: initValue,
required: item.required,
options: optionsValue || [],
props: {
modelValue: initValue,
options: optionsValue || [],
},
};
});
formRules.value = result;
baseInfoLoading.value = false;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const caseTree = ref<ModuleTreeNode[]>([]);
async function initSelectTree() {
try {
caseTree.value = await getCaseModuleTree({ projectId: appStore.currentProjectId });
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
onBeforeMount(() => {
initDefaultFields();
initSelectTree();
});
function handleSave() {
if (activeExtraKey.value === 'baseInfo') {
baseInfoFormRef.value?.validate((errors) => {
if (!errors) {
Message.success(t('common.saveSuccess'));
}
});
}
}
const fileList = ref<MsFileItem[]>([]);
//
function saveSelectAssociatedFile(fileData: AssociatedList[]) {
const fileResultList = fileData.map((fileInfo) => convertToFile(fileInfo));
fileList.value.push(...fileResultList);
}
const getListFunParams = ref<TableQueryParams>({
combine: {
hiddenIds: [],
},
});
//
watch(
() => fileList.value,
(val) => {
if (val) {
getListFunParams.value.combine.hiddenIds = fileList.value.filter((item) => !item.local).map((item) => item.uid);
}
},
{ deep: true }
);
const showLinkFileDrawer = ref<boolean>(false);
function handleFileChange(_fileList: MsFileItem[]) {
fileList.value = _fileList.map((e) => {
return {
...e,
enable: true, //
};
});
}
const imageUrl = ref('');
const previewVisible = ref<boolean>(false);
//
async function handlePreview(item: MsFileItem) {
try {
previewVisible.value = true;
if (item.status !== 'init') {
const res = await previewFile({
projectId: appStore.currentProjectId,
caseId: activeCase.value.id,
fileId: item.uid,
local: item.local,
});
const blob = new Blob([res], { type: 'image/jpeg' });
imageUrl.value = URL.createObjectURL(blob);
} else {
imageUrl.value = item.url || '';
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const transferVisible = ref<boolean>(false);
const activeTransferFileParams = ref<MsFileItem>();
//
function transferFile(item: MsFileItem) {
activeTransferFileParams.value = { ...item };
transferVisible.value = true;
}
//
async function downloadFile(item: MsFileItem) {
try {
const res = await downloadFileRequest({
projectId: appStore.currentProjectId,
caseId: activeCase.value.id,
fileId: item.uid,
local: item.local,
});
downloadByteFile(res, `${item.name}`);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
//
async function handleUpdateFile(item: MsFileItem) {
try {
await updateFile(appStore.currentProjectId, item.associationId);
Message.success(t('common.updateSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
async function handleUploadImage(file: File) {
const { data } = await editorUploadFile({
fileList: [file],
});
return data;
}
const activeComment = ref('caseComment');
const commentTypeOptions = [
{
label: t('caseManagement.featureCase.caseComment'),
value: 'caseComment',
},
{
label: t('caseManagement.featureCase.reviewComment'),
value: 'reviewComment',
},
{
label: t('caseManagement.featureCase.executiveReview'),
value: 'executiveComment',
},
];
const commentList = ref<CommentItem[]>([]);
const reviewCommentList = ref<CommentItem[]>([]);
//
async function initCommentList() {
try {
const result = await getCommentList(activeCase.value.id);
commentList.value = result;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
//
async function initReviewCommentList() {
try {
const result = await getReviewCommentList(activeCase.value.id);
reviewCommentList.value = result;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
//
async function initTestPlanExecuteCommentList() {
try {
const result = await getTestPlanExecuteCommentList(activeCase.value.id);
reviewCommentList.value = result;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
async function getAllCommentList() {
switch (activeComment.value) {
case 'caseComment':
await initCommentList();
break;
case 'reviewComment':
await initReviewCommentList();
break;
case 'executiveComment':
await initTestPlanExecuteCommentList();
break;
default:
break;
}
}
//
async function handleUpdateOrAdd(item: CommentParams, cb: (result: boolean) => void) {
try {
await addOrUpdateCommentList(item);
getAllCommentList();
cb(true);
} catch (error) {
cb(false);
// eslint-disable-next-line no-console
console.error(error);
}
}
//
async function handleDelete(caseCommentId: string) {
openModal({
type: 'error',
title: t('ms.comment.deleteConfirm'),
content: t('ms.comment.deleteContent'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
await deleteCommentList(caseCommentId);
Message.success(t('common.deleteSuccess'));
getAllCommentList();
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
},
hideCancel: false,
});
}
const commentInputRef = ref<InstanceType<typeof inputComment>>();
const content = ref('');
const isActive = ref<boolean>(false);
const noticeUserIds = ref<string[]>([]);
async function publishHandler(currentContent: string) {
try {
const params: CommentParams = {
caseId: activeCase.value.id,
notifier: noticeUserIds.value.join(';'),
replyUser: '',
parentId: '',
content: currentContent,
event: noticeUserIds.value.join(';') ? 'AT' : 'COMMENT', // (: COMMENT; @: AT; /@: REPLAY;)
};
await createCommentList(params);
if (activeExtraKey.value === 'comments') {
getAllCommentList();
}
Message.success(t('common.publishSuccessfully'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
function cancelPublish() {
isActive.value = !isActive.value;
}
const bugList = ref<any[]>([]);
async function initBugList() {
bugList.value = [
{
name: 'sdasd',
id: '3d23d23d',
num: 100001,
},
{
name: 'sdasd',
id: '3d23d23d',
num: 100002,
},
{
name: 'sdasd',
id: '3d23d23d',
num: 100003,
},
{
name: 'sdasd',
id: '3d23d23d',
num: 100004,
},
{
name: 'sdasd',
id: '3d23d23d',
num: 100005,
},
{
name: 'sdasd',
id: '3d23d23d',
num: 100006,
},
{
name: 'sdasd',
id: '3d23d23d',
num: 100007,
},
{
name: 'sdasd',
id: '3d23d23d',
num: 100008,
},
{
name: 'sdasd',
id: '3d23d23d',
num: 100009,
},
];
}
const cancelLoading = ref<boolean>(false);
//
async function disassociateBug(id: string) {
cancelLoading.value = true;
try {
await cancelAssociatedDebug(id);
bugList.value = bugList.value.filter((item) => item.id !== id);
Message.success(t('caseManagement.featureCase.cancelLinkSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
cancelLoading.value = false;
}
}
function goBug(id: string) {
router.push({
name: BugManagementRouteEnum.BUG_MANAGEMENT_INDEX,
query: {
id,
},
});
}
const showCreateBugDrawer = ref<boolean>(false);
function createBug() {
showCreateBugDrawer.value = true;
}
const showLinkBugDrawer = ref<boolean>(false);
function linkBug() {
showLinkBugDrawer.value = true;
}
const drawerLoading = ref<boolean>(false);
async function saveHandler(params: TableQueryParams) {
try {
drawerLoading.value = true;
await associatedDebug(params);
Message.success(t('caseManagement.featureCase.associatedSuccess'));
initBugList();
showLinkBugDrawer.value = false;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
drawerLoading.value = false;
}
}
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
:deep(.commentWrapper) {
right: 0;
}
.bug-list {
.ms-scroll-bar();
overflow-y: auto;
margin-bottom: 16px;
height: calc(100% - 46px);
border-radius: var(--border-radius-small);
.bug-item {
@apply cursor-pointer;
&:not(:last-child) {
margin-bottom: 8px;
}
padding: 8px;
border: 1px solid var(--color-text-n8);
border-radius: var(--border-radius-small);
background-color: white;
&:hover {
@apply relative;
background: var(--color-text-n9);
box-shadow: inset 0 0 0.5px 0.5px rgb(var(--primary-5));
}
}
}
</style>

View File

@ -1,14 +1,14 @@
<template>
<FeatureCaseMinder :import-json="props.importJson" />
<FeatureCaseMinder :module-id="props.moduleId" :module-name="props.moduleName" />
</template>
<script setup lang="ts">
import type { MinderJson } from '@/components/pure/ms-minder-editor/props';
import FeatureCaseMinder from '@/components/business/ms-minders/featureCaseMinder.vue';
const props = defineProps<{
minderType: 'FeatureCase';
importJson: MinderJson;
moduleId: string;
moduleName: string;
}>();
</script>

View File

@ -3,4 +3,7 @@ export default {
'ms.minders.stepDesc': '步骤描述',
'ms.minders.stepExpect': '预期结果',
'ms.minders.textDesc': '文本描述',
'ms.minders.caseName': '用例名称',
'ms.minders.caseNameNotNull': '用例名称不能为空',
'ms.minders.commentTotal': '共 {num} 评论',
};

View File

@ -49,15 +49,18 @@
@enter-node="handleEnterNode"
/>
</div>
<div v-if="props.extractContentTabList?.length" class="ms-minder-editor-extra-content">
<MsTab v-model:activeKey="activeExtraKey" :content-tab-list="props.extractContentTabList" mode="button" />
<div v-if="props.extractContentTabList?.length" class="ms-minder-editor-extra">
<div class="pl-[16px] pt-[16px]">
<MsTab v-model:activeKey="activeExtraKey" :content-tab-list="props.extractContentTabList" mode="button" />
</div>
<div class="ms-minder-editor-extra-content">
<slot name="extractTabContent"></slot>
</div>
</div>
</div>
</template>
<script lang="ts" name="minderEditor" setup>
import { onMounted } from 'vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import minderHeader from './main/header.vue';
import mainEditor from './main/mainEditor.vue';
@ -134,13 +137,18 @@
<style lang="less" scoped>
.ms-minder-editor-container {
@apply relative flex h-full;
.ms-minder-editor-extra-content {
@apply border-l;
.ms-minder-editor-extra {
@apply flex flex-col border-l;
padding: 16px;
width: 35%;
min-width: 360px;
border-color: var(--color-text-n8);
.ms-minder-editor-extra-content {
@apply relative flex-1 overflow-y-auto;
.ms-scroll-bar();
margin-top: 16px;
}
}
}
</style>

View File

@ -44,7 +44,7 @@ export const mainEditorProps = {
default: 500,
},
disabled: Boolean,
extractContentTabList: [] as PropType<{ label: string; value: string }[]>,
extractContentTabList: Array as PropType<{ label: string; value: string }[]>,
};
export const headerProps = {

View File

@ -225,6 +225,7 @@ export interface CommitReviewResultParams {
status: ReviewResult;
content: string;
notifier: string;
reviewCommentFileIds?: string[];
}
// 评审详情-获取用例评审历史
export interface ReviewHistoryItem {

View File

@ -364,3 +364,57 @@ export interface ContentTabsMap {
tabList: TabItemType[];
backupTabList: TabItemType[];
}
// 脑图删除的模块/用例的集合
export interface FeatureCaseMinderDeleteResourceList {
id: string;
type: string;
}
// 脑图新增/修改的模块集合(只记录操作的节点,节点下的子节点不需要记录)
export interface FeatureCaseMinderUpdateModuleList {
id: string;
name: string;
parentId: string;
type: 'ADD' | 'UPDATE'; // 操作类型(新增(ADD)/更新(UPDATE)
moveMode: string;
targetId: string;
}
export interface CustomField {
fieldId: string;
value: string;
}
// 脑图用例步骤描述项
export interface FeatureCaseMinderStepItem {
id: string;
num: number;
desc: string;
result?: string;
actualResult?: string;
executeResult?: string;
}
// 脑图新增/修改的用例对象集合
export interface FeatureCaseMinderUpdateCaseList {
id: string;
templateId: string; // 模板id
type: string;
name: string;
moduleId: string;
moveMode?: string;
targetId?: string;
prerequisite: string;
caseEditType: 'STEP' | 'TEXT'; // 编辑模式
steps: string;
textDescription: string;
expectedResult: string;
description: string;
tags: string[];
customFields: CustomField[];
}
// 脑图
export interface FeatureCaseMinder {
projectId: string;
versionId?: string;
updateCaseList: FeatureCaseMinderUpdateCaseList[];
updateModuleList: FeatureCaseMinderUpdateModuleList[];
deleteResourceList: FeatureCaseMinderDeleteResourceList[];
}

View File

@ -1,5 +1,4 @@
import { defineStore } from 'pinia';
import localforage from 'localforage';
import {
getCaseDefaultFields,

View File

@ -253,11 +253,9 @@
json: parseJson.value,
path: expressionForm.value.expression,
})?.map((e: any) =>
typeof e === 'string'
? e
: JSON.stringify(e)
.replace(/Number\(([^)]+)\)/g, '$1')
.replace(/^"|"$/g, '')
JSON.stringify(e)
.replace(/Number\(([^)]+)\)/g, '$1')
.replace(/^"|"$/g, '')
) || [];
} catch (error) {
matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression }) || [];

View File

@ -299,7 +299,7 @@
input-size="small"
tag-size="small"
@change="(files, file) => handleFileChange(files, record, rowIndex, file)"
@delete-file="() => emitChange('deleteFile')"
@delete-file="() => handleSingleFileDelete(record)"
/>
</template>
<!-- 长度范围 -->
@ -725,6 +725,7 @@
(e: 'projectChange', projectId: string): void;
(e: 'treeDelete', record: Record<string, any>): void;
(e: 'batchAdd'): void;
(e: 'deleteFile', record: Record<string, any>): void;
}>();
const appStore = useAppStore();
@ -1052,6 +1053,17 @@
}
}
function handleSingleFileDelete(record: Record<string, any>) {
record.file = {
fileId: '',
fileName: '',
fileAlias: '',
local: false,
delete: false,
};
emitChange('deleteFile');
}
const showQuickInputParam = ref(false);
const activeQuickInputRecord = ref<any>({});
const quickInputParamValue = ref('');

View File

@ -77,7 +77,7 @@
>
<MsTagsInput
v-model:model-value="mockDetail.tags"
class="w-[732px]"
input-class="w-[732px]"
allow-clear
unique-value
retain-input-value
@ -269,12 +269,8 @@
}
return t('mockManagement.createMock');
});
const activeTab = ref<RequestComposition>(RequestComposition.BODY);
const activeTab = ref<RequestComposition>(RequestComposition.HEADER);
const mockTabList = [
{
value: RequestComposition.BODY,
label: t('apiTestDebug.body'),
},
{
value: RequestComposition.HEADER,
label: t('apiTestDebug.header'),
@ -287,6 +283,10 @@
value: RequestComposition.REST,
label: RequestComposition.REST,
},
{
value: RequestComposition.BODY,
label: t('apiTestDebug.body'),
},
];
/**

View File

@ -210,7 +210,7 @@
];
function handleCsvVariablesChange(resultArr: any[], isInit?: boolean) {
csvVariables.value = [...resultArr];
csvVariables.value = resultArr.map((e) => ({ ...e, enable: e.name && e.fileId }));
if (!isInit) {
emit('change');
}

View File

@ -219,7 +219,7 @@
</div>
<div class="mt-[16px] h-[calc(100%-32px)] border-t border-[var(--color-text-n8)]">
<!-- 脑图开始 -->
<MsMinder minder-type="FeatureCase" :import-json="importJson" @node-click="handleNodeClick" />
<MsMinder minder-type="FeatureCase" :module-id="props.activeFolder" :module-name="props.moduleName" />
<MsDrawer v-model:visible="visible" :width="480" :mask="false">
{{ nodeData.text }}
</MsDrawer>
@ -383,7 +383,7 @@
const props = defineProps<{
activeFolder: string;
activeFolderType: 'folder' | 'module';
moduleName: string;
offspringIds: string[]; // id
modulesCount: Record<string, number>; //
}>();
@ -414,65 +414,11 @@
};
});
});
const moduleId = computed(() => featureCaseStore.moduleId[0]);
const currentProjectId = computed(() => appStore.currentProjectId);
const visible = ref<boolean>(false);
const nodeData = ref<any>({});
const importJson = ref<any>({});
function handleNodeClick(data: any) {
if (data.resource && data.resource.includes('用例')) {
visible.value = true;
nodeData.value = data;
}
}
onBeforeMount(() => {
importJson.value = {
root: {
data: {
text: '测试用例',
id: 'xxxx',
},
children: [
{
data: {
id: 'sdasdas',
text: '模块 1',
resource: ['模块'],
},
},
{
data: {
id: 'dasdasda',
text: '模块 2',
expandState: 'collapse',
},
children: [
{
data: {
id: 'frihofiuho3f',
text: '用例 1',
resource: ['用例'],
},
},
{
data: {
id: 'df09348f034f',
text: ' 用例 2',
resource: ['用例'],
},
},
],
},
],
},
template: 'default',
};
});
const hasOperationPermission = computed(() =>
hasAnyPermission(['FUNCTIONAL_CASE:READ+UPDATE', 'FUNCTIONAL_CASE:READ+DELETE'])
);

View File

@ -239,7 +239,7 @@
offspringIds.push(e.id);
return e;
});
emits('caseNodeSelect', selectedKeys, offspringIds);
emits('caseNodeSelect', selectedKeys, offspringIds, node);
};
//

View File

@ -149,7 +149,7 @@
import { getBugList, getCustomOptionHeader } from '@/api/modules/bug-management';
import {
associatedDrawerDebug,
associatedDebug,
cancelAssociatedDebug,
getLinkedCaseBugList,
} from '@/api/modules/case-management/featureCase';
@ -412,7 +412,7 @@
async function saveHandler(params: TableQueryParams) {
try {
drawerLoading.value = true;
await associatedDrawerDebug(params);
await associatedDebug(params);
Message.success(t('caseManagement.featureCase.associatedSuccess'));
getFetch();
showLinkDrawer.value = false;

View File

@ -0,0 +1,134 @@
<template>
<div class="flex flex-1 flex-col overflow-hidden">
<div class="review-history-list">
<div v-for="item of props.reviewCommentList" :key="item.id" class="review-history-list-item">
<div class="flex items-center">
<MSAvatar :avatar="item.userLogo" />
<div class="ml-[8px] flex items-center">
<a-tooltip :content="item.userName" :mouse-enter-delay="300">
<div class="one-line-text max-w-[300px] font-medium text-[var(--color-text-1)]">{{ item.userName }}</div>
</a-tooltip>
<a-divider direction="vertical" margin="8px"></a-divider>
<div v-if="item.status === 'PASS'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.caseReview.pass') }}
</div>
<div v-else-if="item.status === 'UN_PASS'" class="flex items-center">
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
{{ t('caseManagement.caseReview.fail') }}
</div>
<div v-else-if="item.status === 'UNDER_REVIEWED'" class="flex items-center">
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--link-6))]" />
{{ t('caseManagement.caseReview.suggestion') }}
</div>
<div v-else-if="item.status === 'RE_REVIEWED'" class="flex items-center">
<MsIcon type="icon-icon_resubmit_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
{{ t('caseManagement.caseReview.reReview') }}
</div>
<div v-if="item.status === 'PASSED'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.featureCase.execute.success') }}
</div>
<div v-if="item.status === 'BLOCKED'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
{{ t('caseManagement.featureCase.execute.blocked') }}
</div>
<div v-if="item.status === 'FAILED'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
{{ t('caseManagement.featureCase.execute.failed') }}
</div>
</div>
</div>
<div class="markdown-body" style="margin-left: 48px" v-html="item.contentText"></div>
<div class="ml-[48px] mt-[8px] flex text-[var(--color-text-4)]">
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
<div v-if="props.activeComment === 'reviewComment'">
<a-tooltip :content="item.reviewName" :mouse-enter-delay="300">
<span v-if="item.deleted" class="one-line-text ml-[16px] max-w-[300px] break-words break-all">
{{ characterLimit(item.reviewName) }}
</span>
<span
v-else
class="one-line-text ml-[16px] max-w-[300px] cursor-pointer break-words break-all text-[rgb(var(--primary-5))]"
@click="review(item)"
>
{{ characterLimit(item.reviewName) }}
</span>
</a-tooltip>
</div>
<div v-if="props.activeComment === 'executiveComment'">
<a-tooltip :content="item.testPlanName" :mouse-enter-delay="300">
<span v-if="item.deleted" class="one-line-text ml-[16px] max-w-[300px] break-words break-all">
{{ characterLimit(item.testPlanName) }}
</span>
<span
v-else
class="one-line-text ml-[16px] max-w-[300px] cursor-pointer break-words break-all text-[rgb(var(--primary-5))]"
@click="toPlan(item)"
>
{{ characterLimit(item.testPlanName) }}
</span>
</a-tooltip>
</div>
</div>
</div>
<MsEmpty v-if="reviewCommentList.length === 0" />
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import dayjs from 'dayjs';
import MSAvatar from '@/components/pure/ms-avatar/index.vue';
import MsEmpty from '@/components/pure/ms-empty/index.vue';
import { CommentItem } from '@/components/business/ms-comment/types';
import { useI18n } from '@/hooks/useI18n';
import { characterLimit } from '@/utils';
import { CaseManagementRouteEnum, TestPlanRouteEnum } from '@/enums/routeEnum';
const props = defineProps<{
reviewCommentList: any[];
activeComment: string;
}>();
const router = useRouter();
const route = useRoute();
const { t } = useI18n();
//
function review(record: CommentItem) {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL_CASE_DETAIL,
query: {
...route.query,
caseId: record.caseId,
id: record.reviewId,
},
state: {
params: JSON.stringify(record.moduleName),
},
});
}
//
function toPlan(record: CommentItem) {
router.push({
name: TestPlanRouteEnum.TEST_PLAN_INDEX_DETAIL,
query: {
...route.query,
id: record.testPlanId,
},
state: {
params: JSON.stringify(record.moduleName),
},
});
}
</script>
<style lang="less" scoped></style>

View File

@ -21,89 +21,11 @@
</div>
<!-- 评审评论 -->
<div
<ReviewCommentList
v-show="activeComment === 'reviewComment' || activeComment === 'executiveComment'"
class="flex flex-1 flex-col overflow-hidden"
>
<div class="review-history-list">
<div v-for="item of reviewCommentList" :key="item.id" class="review-history-list-item">
<div class="flex items-center">
<MSAvatar :avatar="item.userLogo" />
<div class="ml-[8px] flex items-center">
<a-tooltip :content="item.userName" :mouse-enter-delay="300">
<div class="one-line-text max-w-[300px] font-medium text-[var(--color-text-1)]">{{
item.userName
}}</div>
</a-tooltip>
<a-divider direction="vertical" margin="8px"></a-divider>
<div v-if="item.status === 'PASS'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.caseReview.pass') }}
</div>
<div v-else-if="item.status === 'UN_PASS'" class="flex items-center">
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
{{ t('caseManagement.caseReview.fail') }}
</div>
<div v-else-if="item.status === 'UNDER_REVIEWED'" class="flex items-center">
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--link-6))]" />
{{ t('caseManagement.caseReview.suggestion') }}
</div>
<div v-else-if="item.status === 'RE_REVIEWED'" class="flex items-center">
<MsIcon type="icon-icon_resubmit_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
{{ t('caseManagement.caseReview.reReview') }}
</div>
<div v-if="item.status === 'PASSED'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.featureCase.execute.success') }}
</div>
<div v-if="item.status === 'BLOCKED'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
{{ t('caseManagement.featureCase.execute.blocked') }}
</div>
<div v-if="item.status === 'FAILED'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
{{ t('caseManagement.featureCase.execute.failed') }}
</div>
</div>
</div>
<div class="markdown-body" style="margin-left: 48px" v-html="item.contentText"></div>
<div class="ml-[48px] mt-[8px] flex text-[var(--color-text-4)]">
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
<div v-if="activeComment === 'reviewComment'">
<a-tooltip :content="item.reviewName" :mouse-enter-delay="300">
<span v-if="item.deleted" class="one-line-text ml-[16px] max-w-[300px] break-words break-all">
{{ characterLimit(item.reviewName) }}
</span>
<span
v-else
class="one-line-text ml-[16px] max-w-[300px] cursor-pointer break-words break-all text-[rgb(var(--primary-5))]"
@click="review(item)"
>
{{ characterLimit(item.reviewName) }}
</span>
</a-tooltip>
</div>
<div v-if="activeComment === 'executiveComment'">
<a-tooltip :content="item.testPlanName" :mouse-enter-delay="300">
<span v-if="item.deleted" class="one-line-text ml-[16px] max-w-[300px] break-words break-all">
{{ characterLimit(item.testPlanName) }}
</span>
<span
v-else
class="one-line-text ml-[16px] max-w-[300px] cursor-pointer break-words break-all text-[rgb(var(--primary-5))]"
@click="toPlan(item)"
>
{{ characterLimit(item.testPlanName) }}
</span>
</a-tooltip>
</div>
</div>
</div>
<MsEmpty v-if="reviewCommentList.length === 0" />
</div>
</div>
:review-comment-list="reviewCommentList"
:active-comment="activeComment"
/>
</div>
</template>
@ -111,12 +33,11 @@
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs';
import MSAvatar from '@/components/pure/ms-avatar/index.vue';
import MsEmpty from '@/components/pure/ms-empty/index.vue';
import MsComment from '@/components/business/ms-comment/comment';
import { CommentItem, CommentParams } from '@/components/business/ms-comment/types';
import ReviewCommentList from './reviewCommentList.vue';
import {
addOrUpdateCommentList,
@ -130,7 +51,6 @@
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { characterLimit } from '@/utils';
import { CaseManagementRouteEnum, TestPlanRouteEnum } from '@/enums/routeEnum';

View File

@ -110,8 +110,8 @@
ref="caseTableRef"
:active-folder="activeFolder"
:offspring-ids="offspringIds"
:active-folder-type="activeCaseType"
:modules-count="modulesCount"
:module-name="activeFolderName"
@init="initModulesCount"
@import="importCase"
></CaseTable>
@ -156,6 +156,7 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsPopConfirm from '@/components/pure/ms-popconfirm/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
import CaseTable from './components/caseTable.vue';
import FeatureCaseTree from './components/caseTree.vue';
import ExportExcelModal from './components/export/exportCaseModal.vue';
@ -191,6 +192,7 @@
};
const activeFolder = ref<string>('all');
const activeFolderName = ref('');
//
const selectedKeys = computed({
@ -220,11 +222,12 @@
const featureCaseStore = useFeatureCaseStore();
//
function caseNodeSelect(keys: string[], _offspringIds: string[]) {
function caseNodeSelect(keys: string[], _offspringIds: string[], node: MsTreeNodeData) {
[activeFolder.value] = keys;
activeCaseType.value = 'module';
offspringIds.value = [..._offspringIds];
featureCaseStore.setModuleId(keys);
activeFolderName.value = node.title || node.name;
}
const confirmLoading = ref(false);

View File

@ -71,6 +71,7 @@
<MsRichText
v-model:raw="caseResultForm.reason"
v-model:commentIds="caseResultForm.commentIds"
v-model:filed-ids="caseResultForm.fileList"
:upload-image="handleUploadImage"
:preview-url="PreviewEditorImageUrl"
class="w-full"
@ -84,7 +85,6 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import { MsFileItem } from '@/components/pure/ms-upload/types';
import { getCaseReviewerList, saveCaseReviewResult } from '@/api/modules/case-management/caseReview';
import { editorUploadFile } from '@/api/modules/case-management/featureCase';
@ -114,7 +114,7 @@
const caseResultForm = ref({
result: 'PASS' as ReviewResult,
reason: '',
fileList: [] as MsFileItem[],
fileList: [] as string[],
commentIds: [] as string[],
});
const submitReviewLoading = ref(false);
@ -152,6 +152,7 @@
reviewPassRule: props.reviewPassRule,
content: caseResultForm.value.reason,
notifier: caseResultForm.value.commentIds.join(';'),
reviewCommentFileIds: caseResultForm.value.fileList,
};
await saveCaseReviewResult(params);
modalVisible.value = false;
@ -169,7 +170,7 @@
caseResultForm.value = {
result: 'PASS' as ReviewResult,
reason: '',
fileList: [] as MsFileItem[],
fileList: [] as string[],
commentIds: [] as string[],
};
if (typeof done === 'function') {