feat(功能用例): 前后置依赖关联&缺陷关联联调&微调组件新增公共脚本业务组件&扩展富文本贴图和附件

This commit is contained in:
xinxin.wu 2024-01-11 10:07:04 +08:00 committed by 刘瑞斌
parent 43cbf5edd5
commit 94c0efafbb
38 changed files with 1856 additions and 240 deletions

View File

@ -51,6 +51,7 @@
"axios": "^0.24.0",
"dayjs": "^1.11.9",
"echarts": "^5.4.3",
"fastq": "^1.15.0",
"hotbox-minder": "1.0.15",
"jsencrypt": "^3.3.2",
"localforage": "^1.10.0",

View File

@ -4,12 +4,14 @@ import MSR from '@/api/http/index';
import {
AddDemandUrl,
AddDependOnRelationUrl,
AssociatedDebuggerUrl,
BatchAssociationDemandUrl,
BatchCopyCaseUrl,
BatchDeleteCaseUrl,
BatchDeleteRecycleCaseListUrl,
BatchEditCaseUrl,
BatchMoveCaseUrl,
CancelAssociatedDebuggerUrl,
CancelAssociationDemandUrl,
cancelPreAndPostCaseUrl,
checkFileIsUpdateUrl,
@ -24,6 +26,8 @@ import {
DetailCaseUrl,
DownloadFileUrl,
FollowerCaseUrl,
GetAssociatedCaseIdsUrl,
GetAssociatedDebuggerUrl,
GetAssociatedDrawerCaseUrl,
GetAssociatedFilePageUrl,
GetAssociationPublicCaseModuleCountUrl,
@ -33,6 +37,7 @@ import {
GetCaseModulesCountUrl,
GetCaseModuleTreeUrl,
GetCommentListUrl,
GetDebugDrawerPageUrl,
GetDefaultTemplateFieldsUrl,
GetDemandListUrl,
GetDependOnPageUrl,
@ -58,6 +63,7 @@ import {
UploadOrAssociationFileUrl,
} from '@/api/requrls/case-management/featureCase';
import type { BugListItem } from '@/models/bug-management';
import type {
AssociatedList,
BatchDeleteType,
@ -325,4 +331,28 @@ export function cancelPreOrPostCase(id: string) {
export function getAssociatedCasePage(data: TableQueryParams) {
return MSR.post<CommonList<CaseManagementTable>>({ url: `${GetAssociatedDrawerCaseUrl}`, data });
}
// 获取用例未关联抽屉缺陷列表
export function getDrawerDebugPage(data: TableQueryParams) {
return MSR.post<CommonList<CaseManagementTable>>({ url: `${GetDebugDrawerPageUrl}`, data });
}
// 关联缺陷
export function associatedDrawerDebug(data: TableQueryParams) {
return MSR.post<CommonList<CaseManagementTable>>({ url: `${AssociatedDebuggerUrl}`, data });
}
// 取消关联缺陷
export function cancelAssociatedDebug(id: string) {
return MSR.get({ url: `${CancelAssociatedDebuggerUrl}/${id}` });
}
// 获取已关联缺陷列表
export function getLinkedCaseBugList(data: TableQueryParams) {
return MSR.post<CommonList<BugListItem>>({ url: `${GetAssociatedDebuggerUrl}`, data });
}
// 获取已关联前后置用例ids
export function getAssociatedCaseIds(caseId: string) {
return MSR.get<string[]>({ url: `${GetAssociatedCaseIdsUrl}/${caseId}` });
}
export default {};

View File

@ -120,3 +120,13 @@ export const cancelPreAndPostCaseUrl = '/functional/case/relationship/delete';
export const publicAssociatedCaseUrl = '/functional/case/test/associate/case';
// 获取关联用例已关联列表
export const GetAssociatedDrawerCaseUrl = '/functional/case/test/has/associate/case/page';
// 获取用例详情缺陷
export const GetDebugDrawerPageUrl = '/functional/case/test/associate/bug/page';
// 关联缺陷
export const AssociatedDebuggerUrl = '/functional/case/test/associate/bug';
// 取消关联缺陷
export const CancelAssociatedDebuggerUrl = '/functional/case/test/disassociate/bug';
// 获取详情已关联缺陷列表
export const GetAssociatedDebuggerUrl = '/functional/case/test/has/associate/bug/page';
// 获取前后置已关联用例ids
export const GetAssociatedCaseIdsUrl = '/functional/case/relationship/get-ids';

View File

@ -367,7 +367,7 @@
(record) => {
return {
...record,
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
tags: (record.tags || []).map((item: string, i: number) => {
return {
id: `${record.id}-${i}`,
name: item,

View File

@ -0,0 +1,344 @@
<template>
<MsDrawer
v-model:visible="exportScriptDrawer"
:title="t('project.commonScript.insertCommonScript')"
:width="1200"
unmount-on-close
:show-continue="false"
:ok-loading="drawerLoading"
:ok-text="t('project.commonScript.apply')"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
<div class="flex h-full">
<div class="w-[292px] border-r border-[var(--color-text-n8)] p-[16px]">
<div class="flex items-center justify-between">
<MsProjectSelect v-model:project="innerProject" class="mb-[16px]" />
<a-select v-model="protocolType" class="mb-[16px] ml-2 max-w-[90px]">
<a-option v-for="item of protocolOptions" :key="item" :value="item">{{ item }}</a-option>
</a-select>
</div>
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('project.commonScript.folderSearchPlaceholder')"
allow-clear
class="mb-[16px]"
/>
<div class="folder">
<div :class="getFolderClass('all')" @click="setActiveFolder('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name">{{ t('project.commonScript.allApis') }}</div>
<div class="folder-count">({{ modulesCount['all'] || 0 }})</div>
</div>
</div>
<a-divider class="my-[8px]" />
<a-spin class="w-full" :loading="moduleLoading">
<MsTree
v-model:selected-keys="selectedModuleKeys"
:data="folderTree"
:keyword="moduleKeyword"
:empty-text="t('project.commonScript.noTreeData')"
:virtual-list-props="virtualListProps"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
block-node
title-tooltip-position="left"
@select="folderNodeSelect"
>
<template #title="nodeData">
<div class="inline-flex w-full">
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
</div>
</template>
</MsTree>
</a-spin>
</div>
<div class="flex w-[calc(100%-293px)] flex-col p-[16px]">
<MsAdvanceFilter
v-model:keyword="keyword"
:filter-config-list="filterConfigList"
:custom-fields-config-list="searchCustomFields"
:row-count="filterRowCount"
:search-placeholder="t('project.commonScript.searchPlaceholder')"
@keyword-search="searchCase"
@adv-search="searchCase"
>
<template #left>
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="mr-[4px] text-[var(--color-text-1)]">{{ activeFolderName }}</div>
<div class="text-[var(--color-text-4)]">({{ propsRes.msPagination?.total }})</div>
</div>
</div>
</template>
</MsAdvanceFilter>
<ms-base-table v-bind="propsRes" no-disable class="mt-[16px]" v-on="propsEvent">
<!-- <template #caseLevel="{ record }">
<caseLevel :case-level="(getCaseLevel(record) as CaseLevel)" />
</template> -->
</ms-base-table>
</div>
</div>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import { FilterFormItem } from '@/components/pure/ms-advance-filter/type';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsProjectSelect from '@/components/business/ms-project-select/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils';
import type { TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/projectManagement/file';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
projectId: string; // id
confirmLoading: boolean;
}>();
const emit = defineEmits(['update:visible', 'save', 'close']);
const drawerLoading = ref<boolean>(false);
const exportScriptDrawer = computed({
get() {
return props.visible;
},
set(val) {
emit('update:visible', val);
},
});
const innerProject = ref(props.projectId);
const moduleKeyword = ref('');
const activeFolder = ref('');
function getFolderClass(id: string) {
return activeFolder.value === id ? 'folder-text folder-text--active' : 'folder-text';
}
const activeFolderName = ref(t('project.commonScript.allApis'));
const selectedModuleKeys = ref<string[]>([]);
function setActiveFolder(id: string) {
activeFolder.value = id;
activeFolderName.value = t('project.commonScript.allApis');
selectedModuleKeys.value = [];
}
const modulesCount = ref<Record<string, any>>({});
const moduleLoading = ref(false);
const folderTree = ref<ModuleTreeNode[]>([]);
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 251px)',
};
});
/**
* 处理模块树节点选中事件
*/
const offspringIds = ref<string[]>([]);
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
selectedModuleKeys.value = _selectedKeys as string[];
activeFolder.value = node.id;
activeFolderName.value = node.name;
offspringIds.value = [];
mapTree(node.children || [], (e) => {
offspringIds.value.push(e.id);
return e;
});
}
const keyword = ref('');
const filterConfigList = ref<FilterFormItem[]>([]);
const searchCustomFields = ref<FilterFormItem[]>([]);
const filterRowCount = ref(0);
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'id',
sortIndex: 1,
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
},
width: 200,
},
{
title: 'project.commonScript.apiName',
dataIndex: 'apiName',
sortable: {
sortDirections: ['ascend', 'descend'],
},
showTooltip: true,
width: 300,
},
{
title: 'project.commonScript.requestType',
dataIndex: 'requestType',
slotName: 'requestType',
sortable: {
sortDirections: ['ascend', 'descend'],
},
width: 200,
},
{
title: 'project.commonScript.responsible',
slotName: 'responsible',
dataIndex: 'responsible',
width: 200,
},
{
title: 'project.commonScript.path',
slotName: 'path',
dataIndex: 'path',
width: 200,
},
{
title: 'ms.case.associate.tags',
dataIndex: 'tags',
slotName: 'tags',
isTag: true,
},
{
title: 'ms.case.associate.version',
slotName: 'version',
width: 200,
},
{
title: 'caseManagement.featureCase.tableColumnCreateUser',
slotName: 'createUser',
dataIndex: 'createUser',
showInTable: true,
width: 300,
},
{
title: 'caseManagement.featureCase.tableColumnCreateTime',
slotName: 'createTime',
dataIndex: 'createTime',
showInTable: true,
width: 300,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
() =>
Promise.resolve({
list: [],
current: 1,
pageSize: 10,
total: 2,
}),
{
scroll: { x: 'auto' },
columns,
showSetting: false,
selectable: true,
showSelectAll: true,
heightUsed: 310,
},
(record) => {
return {
...record,
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
return {
id: `${record.id}-${i}`,
name: item,
};
}),
};
}
);
const protocolType = ref('HTTP'); //
const protocolOptions = ref(['HTTP']);
function searchCase() {}
const searchParams = ref<TableQueryParams>({
moduleIds: [],
});
//
function handleDrawerConfirm() {
const { excludeKeys, selectedKeys, selectorStatus } = propsRes.value;
const { versionId, moduleIds } = searchParams.value;
const params = {
excludeIds: [...excludeKeys],
selectIds: selectorStatus === 'all' ? [] : [...selectedKeys],
selectAll: selectorStatus === 'all',
moduleIds,
versionId,
refId: '',
projectId: innerProject.value,
};
emit('save', params);
}
function handleDrawerCancel() {
exportScriptDrawer.value = false;
resetSelector();
emit('close');
}
</script>
<style scoped lang="less">
.folder {
@apply flex cursor-pointer items-center justify-between;
padding: 8px 4px;
border-radius: var(--border-radius-small);
&:hover {
background-color: rgb(var(--primary-1));
}
.folder-text {
@apply flex cursor-pointer items-center;
.folder-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.folder-name {
color: var(--color-text-1);
}
.folder-count {
margin-left: 4px;
color: var(--color-text-4);
}
}
.folder-text--active {
.folder-icon,
.folder-name,
.folder-count {
color: rgb(var(--primary-5));
}
}
}
.footer {
@apply flex items-center justify-between;
margin: auto -16px -16px;
padding: 12px 16px;
box-shadow: 0 -1px 4px 0 rgb(31 35 41 / 10%);
}
</style>

View File

@ -0,0 +1,189 @@
<template>
<MsDrawer
v-model:visible="insertScriptDrawer"
:title="t('project.commonScript.insertCommonScript')"
:width="768"
unmount-on-close
:show-continue="false"
:ok-loading="drawerLoading"
:ok-text="t('project.commonScript.apply')"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
<div class="mb-4 flex items-center justify-between">
<div class="font-medium">{{ t('project.commonScript.commonScriptList') }}</div>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('caseManagement.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
@search="searchList"
@press-enter="searchList"
></a-input-search>
</div>
<ms-base-table v-bind="propsRes" v-on="propsEvent">
<template #name="{ record }">
<div class="flex items-center">
<div class="one-line-text max-w-[200px] cursor-pointer text-[rgb(var(--primary-5))]">{{ record.name }}</div>
<a-popover :title="t('project.commonScript.publicScriptName')" position="right">
<a-button type="text" class="ml-2 px-0">{{ t('project.commonScript.preview') }}</a-button>
<template #content>
<div class="w-[436px] bg-[var(--color-bg-3)] px-2 pb-2">
<MsCodeEditor
v-model:model-value="record.name"
:show-theme-change="false"
title=""
width="100%"
height="376px"
theme="MS-text"
:read-only="false"
:show-full-screen="false"
/>
</div>
</template>
</a-popover>
</div>
</template>
<template #enable="{ record }">
<MsTag v-if="record.enable" type="success" theme="light">{{ t('project.commonScript.testsPass') }}</MsTag>
<MsTag v-else>{{ t('project.commonScript.draft') }}</MsTag>
</template>
</ms-base-table>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { getDependOnCase } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import debounce from 'lodash-es/debounce';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits(['update:visible', 'save']);
const insertScriptDrawer = computed({
get() {
return props.visible;
},
set(val) {
emit('update:visible', val);
},
});
const keyword = ref<string>('');
function initData() {}
const searchList = debounce(() => {
initData();
}, 100);
const columns: MsTableColumn = [
{
title: 'project.commonScript.name',
dataIndex: 'name',
slotName: 'name',
width: 300,
showInTable: true,
},
{
title: 'project.commonScript.description',
slotName: 'description',
dataIndex: 'description',
width: 200,
showDrag: true,
},
{
title: 'project.commonScript.enable',
dataIndex: 'enable',
slotName: 'enable',
showInTable: true,
width: 150,
showDrag: true,
},
{
title: 'project.commonScript.tags',
dataIndex: 'tags',
slotName: 'tags',
showInTable: true,
isTag: true,
width: 150,
showDrag: true,
},
{
title: 'project.commonScript.createUser',
slotName: 'createUser',
dataIndex: 'createUser',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'project.commonScript.createTime',
slotName: 'createTime',
dataIndex: 'createTime',
sortable: {
sortDirections: ['ascend', 'descend'],
},
showInTable: true,
width: 300,
showDrag: true,
},
{
title: 'system.resourcePool.tableColumnUpdateTime',
dataIndex: 'updateTime',
width: 180,
showDrag: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
getDependOnCase,
{
columns,
scroll: {
x: '100%',
},
showSetting: false,
selectable: true,
heightUsed: 300,
showSelectAll: true,
},
(record) => {
return {
...record,
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
return {
id: `${record.id}-${i}`,
name: item,
};
}),
};
}
);
const drawerLoading = ref<boolean>(false);
function handleDrawerConfirm() {
emit('save');
}
function handleDrawerCancel() {
insertScriptDrawer.value = false;
}
</script>
<style scoped></style>

View File

@ -3,8 +3,14 @@
v-model:visible="showScriptDrawer"
:title="t('ms.case.associate.title')"
:width="768"
:footer="false"
:footer="true"
unmount-on-close
:show-continue="true"
save-continue-text="project.commonScript.saveAsDraft"
ok-text="project.commonScript.apply"
@continue="handleDrawerConfirm(true)"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item
@ -69,7 +75,7 @@
visible: boolean;
}>();
const emit = defineEmits(['update:visible']);
const emit = defineEmits(['update:visible', 'save', 'close']);
const showScriptDrawer = computed({
get() {
@ -140,6 +146,14 @@
);
const scriptType = ref<'commonScript' | 'executionResult'>('commonScript');
function handleDrawerConfirm(isDraft = false) {
emit('save', isDraft);
}
function handleDrawerCancel() {
emit('close');
}
</script>
<style scoped></style>

View File

@ -2,11 +2,11 @@
<div v-if="props.showType === 'commonScript'" class="w-full bg-[var(--color-bg-3)] p-4 pb-0">
<div class="flex items-center justify-between">
<div>
<MsTag class="!mr-2" theme="outline">
<MsTag class="!mr-2 cursor-pointer" theme="outline">
<template #icon><icon-undo class="mr-1 text-[16px] text-[var(--color-text-4)]" /> </template>
{{ t('project.commonScript.undo') }}</MsTag
>
<MsTag theme="outline">
<MsTag theme="outline" class="cursor-pointer">
<template #icon>
<icon-eraser class="mr-1 text-[16px] text-[var(--color-text-4)]" />
</template>
@ -14,44 +14,60 @@
{{ t('project.commonScript.clear') }}</MsTag
>
</div>
<MsTag theme="outline" @click="formatCoding">{{ t('project.commonScript.formatting') }}</MsTag>
<MsTag class="cursor-pointer" theme="outline" @click="formatCoding">{{
t('project.commonScript.formatting')
}}</MsTag>
</div>
</div>
<div v-if="props.showType === 'commonScript'" class="flex h-[calc(100vh-220px)] bg-[var(--color-bg-3)]">
<div class="leftCodeEditor w-[70%]">
<div v-if="props.showType === 'commonScript'" class="flex bg-[var(--color-bg-3)]">
<div class="w-full">
<MsCodeEditor
ref="codeEditorRef"
v-model:model-value="commonScriptValue"
title=""
width="100%"
height="calc(100vh - 255px)"
:width="expanded ? '100%' : '68%'"
height="460px"
theme="MS-text"
:read-only="false"
:show-full-screen="false"
:show-theme-change="false"
/>
</div>
<div class="rightCodeEditor mt-[24px] h-[calc(100vh-255px)] w-[calc(30%-12px)] bg-white">
<div class="flex items-center justify-between p-3">
<div class="flex items-center">
<span v-if="expanded" class="collapsebtn mr-1 flex items-center justify-center" @click="expandedHandler">
>
<template #rightBox>
<div v-if="!expanded" class="w-[32%] min-w-[25%] bg-white p-3 pl-0">
<div class="mb-2 flex items-center justify-between">
<div class="flex items-center">
<span
v-if="expanded"
class="collapsebtn mr-1 flex items-center justify-center"
@click="expandedHandler"
>
<icon-right class="text-[12px] text-[var(--color-text-4)]" />
</span>
<span v-else class="expand mr-1 flex items-center justify-center" @click="expandedHandler">
<icon-down class="text-[12px] text-[rgb(var(--primary-6))]" />
</span>
<div class="font-medium">{{ t('project.commonScript.codeSnippet') }}</div>
</div>
<a-select v-model="language" class="max-w-[50%]" :placeholder="t('project.commonScript.pleaseSelected')">
<a-option v-for="item of languages" :key="item.value">{{ item.text }}</a-option>
</a-select>
</div>
<div class="p-[12px] pt-0">
<div v-for="item of SCRIPT_MENU" :key="item.value" class="menuItem px-1" @click="handleClick(item)">
{{ item.title }}
</div>
</div>
</div>
<span
v-if="expanded"
class="collapsebtn absolute right-2 z-10 mr-1 flex items-center justify-center"
@click="expandedHandler"
>
<icon-right class="text-[12px] text-[var(--color-text-4)]" />
</span>
<span v-else class="expand mr-1 flex items-center justify-center" @click="expandedHandler">
<icon-down class="text-[12px] text-[rgb(var(--primary-6))]" />
</span>
<div class="font-medium">{{ t('project.commonScript.codeSnippet') }}</div>
</div>
<a-select v-model="language" class="max-w-[50%]" :placeholder="t('project.commonScript.pleaseSelected')">
<a-option v-for="item of languages" :key="item.value">{{ item.text }}</a-option>
</a-select>
</div>
<div v-if="!expanded" class="p-[12px] pt-0">
<div v-for="item of SCRIPT_MENU" :key="item.value" class="menuItem px-1" @click="handleClick(item)">
{{ item.title }}
</div>
</div>
</template>
</MsCodeEditor>
</div>
</div>
<MsCodeEditor
@ -65,6 +81,12 @@
:show-full-screen="false"
:show-theme-change="false"
/>
<InsertCommonScript v-model:visible="showInsertDrawer" />
<FormApiImportDrawer
v-model:visible="formApiExportVisible"
:confirm-loading="confirmLoading"
:project-id="currentProjectId"
/>
</template>
<script setup lang="ts">
@ -73,12 +95,18 @@
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import FormApiImportDrawer from './formApiImportDrawer.vue';
import InsertCommonScript from './insertCommonScript.vue';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { CommonScriptMenu } from '@/models/projectManagement/commonScript';
import { getCodeTemplate, type Languages, SCRIPT_MENU } from '../utils';
import { getCodeTemplate, type Languages, SCRIPT_MENU } from './utils';
const appStore = useAppStore();
const currentProjectId = computed(() => appStore.currentProjectId);
const props = defineProps<{
showType: 'commonScript' | 'executionResult'; //
@ -87,7 +115,7 @@
const { t } = useI18n();
const executionResultValue = ref('');
const expanded = ref<boolean>(true);
const expanded = ref<boolean>(false);
const language = ref<Languages>('beanshell');
const commonScriptValue = ref('');
@ -102,14 +130,27 @@
expanded.value = !expanded.value;
}
const showInsertDrawer = ref<boolean>(false);
//
function getCustomFunction() {
showInsertDrawer.value = true;
}
const formApiExportVisible = ref<boolean>(false);
// Api
function getApiExport() {
formApiExportVisible.value = true;
}
function _handleCommand(command) {
switch (command) {
//
case 'custom_function':
getCustomFunction();
return '';
// API
case 'api_definition':
// TODO
getApiExport();
return '';
// API[JSON]
case 'new_api_request': {
@ -125,7 +166,9 @@
const codeEditorRef = ref();
function formatCoding() {}
function formatCoding() {
codeEditorRef.value.format(commonScriptValue.value);
}
function handleCodeTemplate(code: string) {
codeEditorRef.value.insertContent(code);
@ -152,6 +195,8 @@
}
handleCodeTemplate(code);
}
const confirmLoading = ref<boolean>(false);
</script>
<style scoped lang="less">

View File

@ -0,0 +1,519 @@
import { useI18n } from '@/hooks/useI18n';
import type { CommonScriptMenu } from '@/models/projectManagement/commonScript';
const { t } = useI18n();
export type Languages = 'groovy' | 'python' | 'beanshell' | 'nashornScript' | 'rhinoScript' | 'javascript';
export const SCRIPT_MENU: CommonScriptMenu[] = [
{
title: t('project.code_segment.importApiTest'),
value: 'api_definition',
command: 'api_definition',
},
{
title: t('project.code_segment.newApiTest'),
value: 'new_api_request',
command: 'new_api_request',
},
{
title: t('project.processor.codeTemplateGetVariable'),
value: 'vars.get("variable_name")',
},
{
title: t('project.processor.codeTemplateSetVariable'),
value: 'vars.put("variable_name", "variable_value")',
},
{
title: t('project.processor.codeTemplateGetResponseHeader'),
value: 'prev.getResponseHeaders()',
},
{
title: t('project.processor.codeTemplateGetResponseCode'),
value: 'prev.getResponseCode()',
},
{
title: t('project.processor.codeTemplateGetResponseResult'),
value: 'prev.getResponseDataAsString()',
},
{
title: t('project.processor.paramEnvironmentSetGlobalVariable'),
value: `vars.put(\${__metersphere_env_id}+"key","value");
vars.put("key","value")`,
},
{
title: t('project.processor.insertPublicScript'),
value: 'custom_function',
command: 'custom_function',
},
{
title: t('project.processor.terminationTest'),
value: 'ctx.getEngine().stopThreadNow(ctx.getThread().getThreadName());',
},
];
// 处理groovyCode 请求头
function getGroovyHeaders(requestHeaders) {
let headers = '[';
let index = 1;
requestHeaders.forEach(([k, v]) => {
if (index !== 1) {
headers += ',';
}
headers += `'${k}':'${v}'`;
index++;
});
headers += ']';
return headers;
}
// 解析请求url
function getRequestPath(requestArgs, requestPath) {
if (requestArgs.size > 0) {
requestPath += '?';
let index = 1;
requestArgs.forEach(([k, v]) => {
if (index !== 1) {
requestPath += '&';
}
requestPath = `${requestPath + k}=${v}`;
index++;
});
}
return requestPath;
}
// 处理mockPath
function getMockPath(domain, port, socket) {
if (domain === socket || !port) {
return '';
}
const str = `${domain}:${port}`;
// 获取socket之后的路径
return socket.substring(str.length);
}
// 处理请求参数
function replaceRestParams(path, restMap) {
if (!path) {
return path;
}
let arr = path.match(/{([\w]+)}/g);
if (Array.isArray(arr) && arr.length > 0) {
arr = Array.from(new Set(arr));
arr.forEach((str) => {
try {
const temp = str.substr(1);
const param = temp.substring(0, temp.length - 1);
if (str && restMap.has(param)) {
path = path.replace(new RegExp(str, 'g'), restMap.get(param));
}
} catch (e) {
// nothing
}
});
}
return path;
}
// 返回最终groovyCode 代码模板片段
function _groovyCodeTemplate(obj) {
const { requestUrl, requestMethod, headers, body } = obj;
const params = `[
'url': '${requestUrl}',
'method': '${requestMethod}', // POST/GET
'headers': ${headers}, // 请求headers 例:{'Content-type':'application/json'}
'data': ${body} // 参数
]`;
return `import groovy.json.JsonOutput
import groovy.json.JsonSlurper
def params = ${params}
def headers = params['headers']
// json数据
def data = params['data']
def conn = new URL(params['url']).openConnection()
conn.setRequestMethod(params['method'])
if (headers) {
headers.each {
k,v -> conn.setRequestProperty(k, v);
}
}
if (data) {
// 输出请求参数
log.info(data)
conn.doOutput = true
def writer = new OutputStreamWriter(conn.outputStream)
writer.write(data)
writer.flush()
writer.close()
}
log.info(conn.content.text)
`;
}
// 处理groovyCode语言
function groovyCode(requestObj) {
const {
requestHeaders = new Map(),
requestBody = '',
domain = '',
port = '',
requestMethod = '',
host = '',
protocol = '',
requestArguments = new Map(),
requestRest = new Map(),
requestBodyKvs = new Map(),
bodyType,
} = requestObj;
let { requestPath = '' } = requestObj;
let requestUrl = '';
if (requestMethod.toLowerCase() === 'get' && requestBodyKvs) {
// 如果是get方法要将kv值加入argument中
requestBodyKvs.forEach(([k, v]) => {
requestArguments.set(k, v);
});
}
requestPath = getRequestPath(requestArguments, requestPath);
const path = getMockPath(domain, port, host);
requestPath = path + replaceRestParams(requestPath, requestRest);
if (protocol && host && requestPath) {
requestUrl = `${protocol}://${domain}${port ? `:${port}` : ''}${requestPath}`;
}
let body = JSON.stringify(requestBody);
if (requestMethod === 'POST' && bodyType === 'kvs') {
body = '"';
requestBodyKvs.forEach(([k, v]) => {
if (body !== '"') {
body += '&';
}
body += `${k}=${v}`;
});
body += '"';
}
if (bodyType && bodyType.toUpperCase() === 'RAW') {
requestHeaders.set('Content-type', 'text/plain');
}
const headers = getGroovyHeaders(requestHeaders);
const obj = { requestUrl, requestMethod, headers, body };
return _groovyCodeTemplate(obj);
}
// 获取请求头
function getHeaders(requestHeaders) {
let headers = '{';
let index = 1;
requestHeaders.forEach(([k, v]) => {
if (index !== 1) {
headers += ',';
}
// 拼装
headers += `'${k}':'${v}'`;
index++;
});
headers += '}';
return headers;
}
// 获取pythonCode 模板
function _pythonCodeTemplate(obj) {
const { requestBody, requestBodyKvs, bodyType, requestPath, requestMethod, connType, domain, port } = obj;
let { headers } = obj;
let reqBody = obj.requestBody;
if (requestMethod.toLowerCase() === 'post' && obj.bodyType === 'kvs' && obj.requestBodyKvs) {
reqBody = 'urllib.urlencode({';
requestBodyKvs.forEach(([k, v]) => {
reqBody += `'${k}':'${v}'`;
});
reqBody += `})`;
if (headers === '{}') {
headers = "{'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}";
}
}
const host = domain + (port ? `:${port}` : '');
return `import httplib,urllib
params = ${reqBody} # {'username':'test'}
headers = ${headers} # {'Content-Type':'application/json'} {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}
host = '${host}'
path = '${requestPath}'
method = '${requestMethod}' # POST/GET
conn = httplib.${connType}(host)
conn.request(method, path, params, headers)
res = conn.getresponse()
data = unicode(res.read(), 'utf-8')
log.info(data)
`;
}
// 处理pythonCode语言
function pythonCode(requestObj) {
const {
requestHeaders = new Map(),
requestMethod = '',
host = '',
domain = '',
port = '',
protocol = 'http',
requestArguments = new Map(),
requestBodyKvs = new Map(),
bodyType,
requestRest = new Map(),
} = requestObj;
let { requestBody = '', requestPath = '/' } = requestObj;
let connType = 'HTTPConnection';
if (protocol === 'https') {
connType = 'HTTPSConnection';
}
const headers = getHeaders(requestHeaders);
requestBody = requestBody ? JSON.stringify(requestBody) : '{}';
if (requestMethod.toLowerCase() === 'get' && requestBodyKvs) {
requestBodyKvs.forEach(([k, v]) => {
requestArguments.set(k, v);
});
}
requestPath = getRequestPath(requestArguments, requestPath);
const path = getMockPath(domain, port, host);
requestPath = path + replaceRestParams(requestPath, requestRest);
const obj = { requestBody, headers, requestPath, requestMethod, requestBodyKvs, bodyType, connType, domain, port };
return _pythonCodeTemplate(obj);
}
// 获取javaBeanshell代码模版
function _beanshellTemplate(obj) {
const {
requestHeaders = new Map(),
requestBodyKvs = new Map(),
bodyType = '',
requestMethod = 'GET',
protocol = 'http',
requestArguments = new Map(),
domain = '',
host = '',
port = '',
requestRest = new Map(),
} = obj;
let { requestPath = '/', requestBody = '' } = obj;
const path = getMockPath(domain, port, host);
requestPath = path + replaceRestParams(requestPath, requestRest);
let uri = `new URIBuilder()
.setScheme("${protocol}")
.setHost("${domain}")
.setPath("${requestPath}")
`;
// http 请求类型
const method = requestMethod.toLowerCase().replace(/^\S/, (s) => s.toUpperCase());
const httpMethodCode = `Http${method} request = new Http${method}(uri);`;
// 设置参数
requestArguments.forEach(([k, v]) => {
uri += `.setParameter("${k}", "${v}")`;
});
if (method === 'Get' && requestBodyKvs) {
requestBodyKvs.forEach(([k, v]) => {
uri += `.setParameter("${k}", "${v}")`;
});
}
let postKvsParam = '';
if (method === 'Post') {
// 设置post参数
requestBodyKvs.forEach(([k, v]) => {
postKvsParam += `nameValueList.add(new BasicNameValuePair("${k}", "${v}"));\r\n`;
});
if (postKvsParam !== '') {
postKvsParam = `List nameValueList = new ArrayList();\r\n${postKvsParam}`;
}
}
if (port) {
uri += `.setPort(${port}) // int类型端口
`;
uri += ` .build();`;
} else {
uri += `// .setPort(${port}) // int类型端口
`;
uri += ` .build();`;
}
// 设置请求头
let setHeader = '';
requestHeaders.forEach(([k, v]) => {
setHeader = `${setHeader}request.setHeader("${k}", "${v}");\n`;
});
try {
requestBody = JSON.stringify(requestBody);
if (!requestBody || requestBody === 'null') {
requestBody = '';
}
} catch (e) {
requestBody = '';
}
let postMethodCode = '';
if (requestMethod === 'POST') {
if (bodyType === 'kvs') {
postMethodCode = `${postKvsParam}\r\n request.setEntity(new UrlEncodedFormEntity(nameValueList, "UTF-8"));`;
} else {
postMethodCode = `request.setEntity(new StringEntity(StringEscapeUtils.unescapeJava(payload)));`;
}
}
return `import java.net.URI;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.*;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.http.entity.StringEntity;
import java.util.*;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 参数
String payload = ${requestBody};
// 定义请求的参数
URI uri = ${uri}
// 创建http请求
${httpMethodCode}
${setHeader}
${postMethodCode}
log.info(uri.toString());
//response 对象
CloseableHttpResponse response = null;
response = httpclient.execute(request);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
log.info(content);
}`;
}
// 处理java语言
function javaCode(requestObj) {
return _beanshellTemplate(requestObj);
}
// 获取js语言代码模版
function _jsTemplate(obj) {
const {
requestHeaders = new Map(),
requestMethod = 'GET',
protocol = 'http',
requestArguments = new Map(),
host = '',
domain = '',
port = '',
requestBodyKvs = new Map(),
bodyType = '',
requestRest = new Map(),
} = obj;
let url = '';
let { requestBody = '', requestPath = '/' } = obj;
requestPath = replaceRestParams(requestPath, requestRest);
if (protocol && domain && port) {
const path = getMockPath(domain, port, host);
requestPath = path + requestPath;
url = `${protocol}://${domain}${port ? `:${port}` : ''}${requestPath}`;
} else if (protocol && domain) {
url = `${protocol}://${domain}${requestPath}`;
}
if (requestMethod.toLowerCase() === 'get' && requestBodyKvs) {
// 如果是get方法要将kv值加入argument中
requestBodyKvs.forEach(([k, v]) => {
requestArguments.set(k, v);
});
}
url = getRequestPath(requestArguments, url);
try {
requestBody = JSON.stringify(requestBody);
} catch (e) {
requestBody = '';
}
let connStr = '';
if (bodyType && bodyType.toUpperCase() === 'RAW') {
requestHeaders.set('Content-type', 'text/plain');
}
requestHeaders.forEach(([k, v]) => {
connStr += `conn.setRequestProperty("${k}","${v}");\n`;
});
if (requestMethod === 'POST' && bodyType === 'kvs') {
requestBody = '"';
requestBodyKvs.forEach(([k, v]) => {
if (requestBody !== '"') {
requestBody += '&';
}
requestBody += `${k}=${v}`;
});
requestBody += '"';
}
let postParamExecCode = '';
if (requestBody && requestBody !== '' && requestBody !== '""') {
postParamExecCode = `
var opt = new java.io.DataOutputStream(conn.getOutputStream());
var t = (new java.lang.String(parameterData)).getBytes("utf-8");
opt.write(t);
opt.flush();
opt.close();
`;
}
return `var urlStr = "${url}"; // 请求地址
var requestMethod = "${requestMethod}"; // 请求类型
var parameterData = ${requestBody}; // 请求参数
var url = new java.net.URL(urlStr);
var conn = url.openConnection();
conn.setRequestMethod(requestMethod);
conn.setDoOutput(true);
${connStr}conn.connect();
${postParamExecCode}
var res = "";
var rspCode = conn.getResponseCode();
if (rspCode == 200) {
var ipt = conn.getInputStream();
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(ipt, "UTF-8"));
var lines;
while((lines = reader.readLine()) !== null) {
res += lines;
}
}
log.info(res);
`;
}
// 处理js语言
function jsCode(requestObj) {
return _jsTemplate(requestObj);
}
export function getCodeTemplate(language: Languages, requestObj: any) {
switch (language) {
case 'groovy':
return groovyCode(requestObj);
case 'python':
return pythonCode(requestObj);
case 'beanshell':
return javaCode(requestObj);
case 'nashornScript':
return jsCode(requestObj);
case 'rhinoScript':
return jsCode(requestObj);
case 'javascript':
return jsCode(requestObj);
default:
return '';
}
}
export default {};

View File

@ -19,7 +19,10 @@
{{ t('msCodeEditor.fullScreen') }}
</div>
</div>
<div ref="codeEditBox" :class="['ms-code-editor', isFullscreen ? 'ms-code-editor-full-screen' : '']"></div>
<div class="flex w-full flex-row">
<div ref="codeEditBox" :class="['ms-code-editor', isFullscreen ? 'ms-code-editor-full-screen' : '']"></div>
<slot name="rightBox"> </slot>
</div>
</div>
</template>
@ -33,6 +36,8 @@
import MsCodeEditorTheme from './themes';
import { CustomTheme, editorProps, Theme } from './types';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import prettier from 'prettier';
import parserBabel from 'prettier/parser-babel';
export default defineComponent({
name: 'MonacoEditor',

View File

@ -14,12 +14,18 @@
*/
import { useDebounceFn, useLocalStorage } from '@vueuse/core';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import AttachmentSelectorModal from './attachmentSelectorModal.vue';
import { useI18n } from '@/hooks/useI18n';
import useLocale from '@/locale/useLocale';
import '@halo-dev/richtext-editor/dist/style.css';
import suggestion from './extensions/mention/suggestion';
import {
type AnyExtension,
Editor,
Extension,
ExtensionAudio,
ExtensionBlockquote,
ExtensionBold,
@ -60,8 +66,20 @@
ExtensionVideo,
lowlight,
RichTextEditor,
ToolbarItem,
ToolboxItem,
} from '@halo-dev/richtext-editor';
import Mention from '@tiptap/extension-mention';
import type { queueAsPromised } from 'fastq';
import * as fastq from 'fastq';
const { t } = useI18n();
// image drag and paste upload
type Task = {
file: File;
process: (permalink: string) => void;
};
const props = withDefaults(
defineProps<{
@ -81,92 +99,17 @@
(event: 'update', value: string): void;
}>();
// debounce OnUpdate
const debounceOnUpdate = useDebounceFn(() => {
const html = `${editor.value?.getHTML()}`;
emit('update:raw', html);
emit('update', html);
}, 250);
async function asyncWorker(arg: Task): Promise<void> {
if (!props.uploadImage) {
return;
}
const attachmentData = await props.uploadImage(arg.file);
if (attachmentData.status?.permalink) {
arg.process(attachmentData.status.permalink);
}
}
editor.value = new Editor({
content: props.raw,
extensions: [
ExtensionBlockquote,
ExtensionBold,
ExtensionBulletList,
ExtensionCode,
ExtensionDocument,
ExtensionDropcursor.configure({
width: 2,
class: 'dropcursor',
color: 'skyblue',
}),
ExtensionCommands,
ExtensionGapcursor,
ExtensionHardBreak,
ExtensionHeading,
ExtensionHistory,
ExtensionHorizontalRule,
ExtensionItalic,
ExtensionOrderedList,
ExtensionStrike,
ExtensionText,
ExtensionImage.configure({
inline: true,
allowBase64: false,
HTMLAttributes: {
loading: 'lazy',
},
}),
ExtensionTaskList,
ExtensionLink.configure({
autolink: false,
openOnClick: false,
}),
ExtensionTextAlign.configure({
types: ['heading', 'paragraph'],
}),
ExtensionUnderline,
ExtensionTable.configure({
resizable: true,
}),
ExtensionSubscript,
ExtensionSuperscript,
ExtensionPlaceholder.configure({
placeholder: '输入 / 以选择输入类型',
}),
ExtensionHighlight,
ExtensionVideo,
ExtensionAudio,
ExtensionCodeBlock.configure({
lowlight,
}),
ExtensionIframe,
ExtensionColor,
ExtensionFontSize,
ExtensionIndent,
ExtensionDraggable,
ExtensionColumns,
ExtensionColumn,
ExtensionNodeSelected,
ExtensionTrailingNode,
Mention.configure({
HTMLAttributes: {
class: 'mention',
},
// TODO userMap
renderLabel({ options, node }) {
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;
// return `${options.suggestion.char}${userMap[node.attrs.id]}`;
},
suggestion,
}),
],
autofocus: 'start',
onUpdate: () => {
debounceOnUpdate();
},
});
const uploadQueue: queueAsPromised<Task> = fastq.promise(asyncWorker, 1);
const { currentLocale } = useLocale();
const locale = computed(() => currentLocale.value as 'zh-CN' | 'en-US');
@ -183,6 +126,236 @@
}
);
const showSidebar = useLocalStorage('halo:editor:show-sidebar', true);
const attachmentSelectorModal = ref(false);
onMounted(() => {
const debounceOnUpdate = useDebounceFn(() => {
const html = `${editor.value?.getHTML()}`;
emit('update:raw', html);
emit('update', html);
}, 250);
editor.value = new Editor({
content: props.raw,
extensions: [
ExtensionBlockquote,
ExtensionBold,
ExtensionBulletList,
ExtensionCode,
ExtensionDocument,
ExtensionDropcursor.configure({
width: 2,
class: 'dropcursor',
color: 'skyblue',
}),
ExtensionCommands,
ExtensionGapcursor,
ExtensionHardBreak,
ExtensionHeading,
ExtensionHistory,
ExtensionHorizontalRule,
ExtensionItalic,
ExtensionOrderedList,
ExtensionStrike,
ExtensionText,
ExtensionImage.configure({
inline: true,
allowBase64: false,
HTMLAttributes: {
loading: 'lazy',
},
}),
ExtensionTaskList,
ExtensionLink.configure({
autolink: false,
openOnClick: false,
}),
ExtensionTextAlign.configure({
types: ['heading', 'paragraph'],
}),
ExtensionUnderline,
ExtensionTable.configure({
resizable: true,
}),
ExtensionSubscript,
ExtensionSuperscript,
ExtensionPlaceholder.configure({
placeholder: '输入 / 以选择输入类型',
}),
ExtensionHighlight,
ExtensionVideo,
ExtensionAudio,
ExtensionCodeBlock.configure({
lowlight,
}),
ExtensionIframe,
ExtensionColor,
ExtensionFontSize,
ExtensionIndent,
Extension.create({
addGlobalAttributes() {
return [
{
types: ['heading'],
attributes: {
id: {
default: null,
},
},
},
];
},
}),
Extension.create({
addOptions() {
return {
getToolboxItems({ editors }: { editors: Editor }) {
return [
{
priority: 0,
component: markRaw(ToolboxItem),
props: {
editor,
// icon: () => {
// return defineComponent({
// template: "<MsIcon type='icon-icon_link-copy_outlined' size='16' />",
// });
// },
title: t('editor.attachment'),
action: () => {
attachmentSelectorModal.value = true;
},
},
},
];
},
getToolbarItems({ editors }: { editors: Editor }) {
return {
priority: 1000,
component: markRaw(ToolbarItem),
props: {
editor,
isActive: showSidebar.value,
// icon: markRaw(RiLayoutRightLine),
title: t(''),
action: () => {
showSidebar.value = !showSidebar.value;
},
},
};
},
};
},
}),
ExtensionDraggable,
ExtensionColumns,
ExtensionColumn,
ExtensionNodeSelected,
ExtensionTrailingNode,
Mention.configure({
HTMLAttributes: {
class: 'mention',
},
// TODO userMap
renderLabel({ options, node }) {
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;
// return `${options.suggestion.char}${userMap[node.attrs.id]}`;
},
suggestion,
}),
],
autofocus: 'start',
onUpdate: () => {
debounceOnUpdate();
},
editorProps: {
handleDrop: (view, event: DragEvent, _, moved) => {
debugger;
if (!moved && event.dataTransfer && event.dataTransfer.files) {
const images = Array.from(event.dataTransfer.files).filter((file) =>
file.type.startsWith('image/')
) as File[];
if (images.length === 0) {
return;
}
event.preventDefault();
images.forEach((file, index) => {
uploadQueue.push({
file,
process: (url: string) => {
const { schema } = view.state;
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (!coordinates) return;
const node = schema.nodes.image.create({
src: url,
});
const transaction = view.state.tr.insert(coordinates.pos + index, node);
editor.value?.view.dispatch(transaction);
},
});
});
return true;
}
return false;
},
handlePaste: (view, event: ClipboardEvent) => {
const types = Array.from(event.clipboardData?.types || []);
if (['text/plain', 'text/html'].includes(types[0])) {
return;
}
const images = Array.from(event.clipboardData?.items || [])
.map((item) => {
return item.getAsFile();
})
.filter((file) => {
return file && file.type.startsWith('image/');
}) as File[];
if (images.length === 0) {
return;
}
event.preventDefault();
images.forEach((file) => {
uploadQueue.push({
file,
process: (url: string) => {
editor.value
?.chain()
.focus()
.insertContent([
{
type: 'image',
attrs: {
src: url,
},
},
])
.run();
},
});
});
},
},
});
});
onBeforeUnmount(() => {
editor.value?.destroy();
});
@ -190,6 +363,7 @@
<template>
<div class="rich-wrapper flex w-full">
<AttachmentSelectorModal v-model:visible="attachmentSelectorModal" />
<RichTextEditor v-if="editor" :editor="editor" :locale="locale" />
</div>
</template>

View File

@ -0,0 +1,53 @@
<template>
<a-modal v-model:visible="uploadVisible" class="ms-modal-form ms-modal-small" title-align="start">
<template #title> {{ t('editor.attachment') }} </template>
<MsUpload
v-model:file-list="fileList"
class="w-full"
accept="none"
:max-size="50"
size-unit="MB"
main-text="system.user.importModalDragText"
:sub-text="t('system.plugin.supportFormat')"
:show-file-list="false"
:auto-upload="false"
:disabled="confirmLoading"
></MsUpload>
<template #footer> </template>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import { useI18n } from '@/hooks/useI18n';
import type { FileItem } from '@arco-design/web-vue';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
}>();
const emits = defineEmits<{
(e: 'update:visible', value: boolean): void;
}>();
const uploadVisible = computed({
get() {
return props.visible;
},
set(val) {
emits('update:visible', val);
},
});
const fileList = ref<FileItem[]>([]);
const confirmLoading = ref<boolean>(false);
</script>
<style scoped></style>

View File

@ -0,0 +1,3 @@
export default {
'editor.attachment': 'Upload attachments',
};

View File

@ -0,0 +1,3 @@
export default {
'editor.attachment': '上传附件',
};

View File

@ -0,0 +1,28 @@
import { RouteRecordName, RouteRecordRaw } from 'vue-router';
export interface RouteRecordAppend {
parentName: RouteRecordName;
route: RouteRecordRaw;
}
export interface PluginModule {
/**
* These components will be registered when plugin is activated.
*/
components?: Record<string, Component>;
/**
* Activate hook will be called when plugin is activated.
*/
activated?: () => void;
/**
* Deactivate hook will be called when plugin is deactivated.
*/
deactivated?: () => void;
routes?: RouteRecordRaw[] | RouteRecordAppend[];
ucRoutes?: RouteRecordRaw[] | RouteRecordAppend[];
extensionPoints?: any;
}

View File

@ -163,11 +163,13 @@
//
function disableDefaultEvents() {
const doc = document.documentElement;
doc.addEventListener('dragleave', (e) => e.preventDefault()); //
doc.addEventListener('drop', (e) => e.preventDefault()); //
doc.addEventListener('dragenter', (e) => e.preventDefault()); //
doc.addEventListener('dragover', (e) => e.preventDefault()); //
const doc = document.querySelector('body');
if (doc) {
doc.addEventListener('dragleave', (e) => e.preventDefault()); //
doc.addEventListener('drop', (e) => e.preventDefault()); //
doc.addEventListener('dragenter', (e) => e.preventDefault()); //
doc.addEventListener('dragover', (e) => e.preventDefault()); //
}
}
const menuWidth = ref<number>();
@ -186,8 +188,12 @@
resizeObserver.value.observe(targetElement.value);
menuWidth.value = targetElement.value.getBoundingClientRect().width;
if (ele) {
ele.addEventListener('dragenter', () => {
showDropArea.value = true;
ele.addEventListener('dragenter', (event) => {
const { dataTransfer } = event;
if (dataTransfer && dataTransfer.types.includes('Files')) {
//
showDropArea.value = true;
}
});
//
ele.addEventListener('dragleave', (e: any) => {

View File

@ -37,7 +37,7 @@ const useFeatureCaseStore = defineStore('featureCase', {
this.caseTree = tree;
},
// 获取模块数量
async getCaseModulesCountCount(params: CaseModuleQueryParams) {
async getCaseModulesCount(params: CaseModuleQueryParams) {
try {
this.modulesCount = {};
this.modulesCount = await getCaseModulesCounts(params);
@ -46,7 +46,7 @@ const useFeatureCaseStore = defineStore('featureCase', {
}
},
// 获取模块数量
async getRecycleMModulesCountCount(params: CaseModuleQueryParams) {
async getRecycleModulesCount(params: CaseModuleQueryParams) {
try {
this.recycleModulesCount = {};
this.recycleModulesCount = await getRecycleModulesCounts(params);

View File

@ -71,6 +71,10 @@
if (route.params.mode === 'edit') {
await updateCaseRequest(caseDetailInfo.value);
Message.success(t('caseManagement.featureCase.editSuccess'));
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE,
query: { organizationId: route.query.organizationId, projectId: route.query.projectId },
});
} else {
const res = await createCaseRequest(caseDetailInfo.value);
if (isReview) {
@ -79,6 +83,17 @@
}
createSuccessId.value = res.data.id;
Message.success(route.params.mode === 'copy' ? t('ms.description.copySuccess') : t('common.addSuccess'));
featureCaseStore.setIsAlreadySuccess(true);
isShowTip.value = !getIsVisited();
if (isShowTip.value && !route.query.id) {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_CREATE_SUCCESS,
query: {
id: createSuccessId.value,
...route.query,
},
});
}
}
if (isReview) {
router.push({
@ -89,20 +104,6 @@
projectId: route.query.projectId,
},
});
} else {
router.push({ name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE, query: { ...route.query } });
}
featureCaseStore.setIsAlreadySuccess(true);
isShowTip.value = !getIsVisited();
if (isShowTip.value && !route.query.id) {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_CREATE_SUCCESS,
query: {
id: createSuccessId.value,
...route.query,
},
});
}
} catch (error) {
console.log(error);

View File

@ -11,6 +11,7 @@
:pagination="props.pagination"
:table-data="props.tableData"
:page-change="props.pageChange"
:mask-closable="false"
@loaded="loadedCase"
>
<template #titleLeft>
@ -118,7 +119,7 @@
/>
<TabDemand v-else-if="activeTab === 'requirement'" :case-id="props.detailId" />
<TabCaseTable v-else-if="activeTab === 'case'" :case-id="props.detailId" />
<TabDefect v-else-if="activeTab === 'bug'" />
<TabDefect v-else-if="activeTab === 'bug'" :case-id="props.detailId" />
<TabDependency v-else-if="activeTab === 'dependency'" :case-id="props.detailId" />
<TabCaseReview v-else-if="activeTab === 'caseReview'" :case-id="props.detailId" />
<TabTestPlan v-else-if="activeTab === 'testPlan'" />

View File

@ -6,6 +6,7 @@
:custom-fields-config-list="searchCustomFields"
:row-count="filterRowCount"
@keyword-search="fetchData"
@adv-search="handleAdvSearch"
>
<template #left>
<div class="text-[var(--color-text-1)]"
@ -198,7 +199,7 @@
import MinderEditor from '@/components/pure/minder-editor/minderEditor.vue';
import { CustomTypeMaps, MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import { FilterFormItem, FilterType } from '@/components/pure/ms-advance-filter/type';
import { FilterFormItem, FilterResult, FilterType } from '@/components/pure/ms-advance-filter/type';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { FieldTypeFormRules } from '@/components/pure/ms-form-create/form-create';
@ -557,11 +558,6 @@
dataIndex: 'createTime',
type: FilterType.DATE_PICKER,
},
{
title: 'bugManagement.createTime',
dataIndex: 'createTime',
type: FilterType.DATE_PICKER,
},
{
title: 'caseManagement.featureCase.tableColumnUpdateUser',
dataIndex: 'updateUser',
@ -644,7 +640,7 @@
}
}
const initDefaultFields = ref<CustomAttributes[]>([]);
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setKeyword } = useTable(
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setKeyword, setAdvanceFilter } = useTable(
getCaseList,
{
tableKey: TableKeyEnum.CASE_MANAGEMENT_TABLE,
@ -695,7 +691,7 @@
return {
...record,
...recordMap,
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
tags: (record.tags || []).map((item: string, i: number) => {
return {
id: `${record.id}-${i}`,
name: item,
@ -1131,13 +1127,33 @@
console.log(error);
}
}
const filterResult = ref<FilterResult>({ accordBelow: 'AND', combine: {} });
//
const currentSelectParams = ref<BatchActionQueryParams>({ selectAll: false, currentSelectCount: 0 });
//
const handleAdvSearch = (filter: FilterResult) => {
filterResult.value = filter;
const { accordBelow, combine } = filter;
setAdvanceFilter(filter);
currentSelectParams.value = {
...currentSelectParams.value,
condition: {
keyword: keyword.value,
searchMode: accordBelow,
filter: propsRes.value.filter,
combine,
},
};
initData();
};
onBeforeMount(() => {
onMounted(() => {
if (route.query.id) {
showCaseDetail(route.query.id as string, 0);
}
getDefaultFields();
initFilter();
initData();
});
watch(
@ -1153,8 +1169,7 @@
keyword.value = '';
initData();
resetSelector();
},
{ immediate: true }
}
);
</script>

View File

@ -18,7 +18,7 @@
></a-input>
</a-form-item>
<a-form-item field="precondition" :label="t('system.orgTemplate.precondition')" asterisk-position="end">
<MsRichText v-model:raw="form.prerequisite" />
<MsRichText v-model:raw="form.prerequisite" :upload-image="handleUploadImage" />
</a-form-item>
<a-form-item
field="step"
@ -170,6 +170,7 @@
key: 'id',
children: 'children',
}"
:draggable="false"
:tree-props="{
virtualListProps: {
height: 200,
@ -244,13 +245,14 @@
previewFile,
transferFileRequest,
updateFile,
uploadOrAssociationFile,
} from '@/api/modules/case-management/featureCase';
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
import { getProjectFieldList } from '@/api/modules/setting/template';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { downloadByteFile, getGenerateId } from '@/utils';
import { downloadByteFile, getGenerateId, mapTree } from '@/utils';
import type {
AssociatedList,
@ -260,6 +262,7 @@
StepList,
} from '@/models/caseManagement/featureCase';
import type { TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/projectManagement/file';
import type { CustomField, DefinedFieldItem } from '@/models/setting/template';
import { convertToFile } from './utils';
@ -298,7 +301,14 @@
const featureCaseStore = useFeatureCaseStore();
const modelId = computed(() => featureCaseStore.moduleId[0]);
const caseTree = computed(() => featureCaseStore.caseTree);
const caseTree = computed(() => {
return mapTree<ModuleTreeNode>(featureCaseStore.caseTree, (e) => {
return {
...e,
draggable: false,
};
});
});
const initForm: DetailCase = {
id: '',
@ -481,11 +491,10 @@
//
function getDetailData(detailResult: DetailCase) {
const { customFields, attachments, steps, tags } = detailResult;
const { customFields, attachments, steps } = detailResult;
form.value = {
...detailResult,
name: route.params.mode === 'copy' ? `${detailResult.name}_copy` : detailResult.name,
tags: JSON.parse(tags),
};
//
selectData.value = getCustomDetailFields(
@ -718,6 +727,17 @@
}
}
async function handleUploadImage(file: File) {
const { data } = await uploadOrAssociationFile({
request: {
caseId: route.query.id,
projectId: currentProjectId.value,
},
fileList: [file],
});
return data;
}
defineExpose({
caseFormRef,
formRef,

View File

@ -118,6 +118,7 @@
}
function goDetail() {
clearInterval(timer.value);
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE,
query: route.query,

View File

@ -466,7 +466,7 @@
//
function initRecycleModulesCount() {
featureCaseStore.getRecycleMModulesCountCount(emitTableParams);
featureCaseStore.getRecycleModulesCount(emitTableParams);
}
const batchParams = ref<BatchActionQueryParams>({

View File

@ -137,9 +137,15 @@
testPlanTab.push(...moduleTab.value[item.module]);
}
});
tabDefaultSettingList.value.splice(1, 0, buggerTab[0], buggerTab[1]);
tabDefaultSettingList.value.splice(-2, 0, testPlanTab[0]);
featureCaseStore.setTab(tabDefaultSettingList.value);
const newTabDefaultSettingList = [
tabDefaultSettingList.value[0],
...buggerTab,
...tabDefaultSettingList.value.slice(1, -2),
...testPlanTab,
tabDefaultSettingList.value[tabDefaultSettingList.value.length - 2],
tabDefaultSettingList.value[tabDefaultSettingList.value.length - 1],
];
featureCaseStore.setTab(newTabDefaultSettingList);
}
const tabList = computed(() => featureCaseStore.tabSettingList);

View File

@ -6,6 +6,7 @@
:ok-text="t('common.confirm')"
:ok-loading="drawerLoading"
:width="800"
:mask-closable="false"
unmount-on-close
:show-continue="true"
@continue="handleDrawerConfirm(true)"

View File

@ -4,10 +4,12 @@
:mask="false"
:title="t('caseManagement.featureCase.linkDefect')"
:ok-text="t('caseManagement.featureCase.associated')"
:ok-loading="drawerLoading"
:width="960"
:ok-disabled="propsRes.selectedKeys.size === 0"
:width="1200"
:mask-closable="false"
unmount-on-close
:show-continue="false"
:ok-loading="props.drawerLoading"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
@ -24,9 +26,16 @@
</div>
<div>
<ms-base-table ref="tableRef" v-bind="propsRes" v-on="propsEvent">
<template #defectName="{ record }">
<span class="one-line-text max-w[300px]"> {{ record.name }}</span
><span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
<template #name="{ record }">
<span class="one-line-text max-w-[300px]"> {{ record.name }}</span>
<a-popover title="" position="right">
<span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
<template #content>
<div class="max-w-[600px] text-[14px] text-[var(--color-text-1)]">
{{ record.name }}
</div>
</template>
</a-popover>
</template>
</ms-base-table>
</div>
@ -41,18 +50,24 @@
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { getDrawerDebugPage } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const appStore = useAppStore();
const currentProjectId = computed(() => appStore.currentProjectId);
const props = defineProps<{
visible: boolean;
caseId: string;
drawerLoading: boolean;
}>();
const emit = defineEmits(['update:visible']);
const emit = defineEmits(['update:visible', 'save']);
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.tableColumnID',
@ -65,8 +80,8 @@
},
{
title: 'caseManagement.featureCase.defectName',
slotName: 'defectName',
dataIndex: 'defectName',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: true,
width: 300,
@ -75,8 +90,8 @@
},
{
title: 'caseManagement.featureCase.updateUser',
slotName: 'name',
dataIndex: 'updateUser',
slotName: 'handleUserName',
dataIndex: 'handleUserName',
showInTable: true,
showTooltip: true,
width: 300,
@ -85,8 +100,8 @@
},
{
title: 'caseManagement.featureCase.defectState',
slotName: 'defectState',
dataIndex: 'defectState',
slotName: 'status',
dataIndex: 'status',
showInTable: true,
showTooltip: true,
width: 300,
@ -94,29 +109,45 @@
showDrag: false,
},
{
title: 'caseManagement.featureCase.IterationPlan',
dataIndex: 'level',
title: 'caseManagement.featureCase.defectSource',
slotName: 'defectSource',
dataIndex: 'defectSource',
showInTable: true,
width: 200,
showTooltip: true,
width: 200,
ellipsis: true,
showDrag: true,
showDrag: false,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getRecycleListRequest, {
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getDrawerDebugPage, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEFECT,
selectable: true,
scroll: { x: 1000 },
scroll: { x: 'auto' },
heightUsed: 340,
enableDrag: true,
enableDrag: false,
});
const drawerLoading = ref<boolean>(false);
const keyword = ref<string>('');
function handleDrawerConfirm() {}
function handleDrawerCancel() {}
function handleDrawerConfirm() {
const { excludeKeys, selectedKeys, selectorStatus } = propsRes.value;
const params = {
excludeIds: [...excludeKeys],
selectIds: selectorStatus === 'all' ? [] : [...selectedKeys],
selectAll: selectorStatus === 'all',
projectId: currentProjectId.value,
keyword: keyword.value,
searchMode: 'AND',
combine: {},
caseId: props.caseId,
};
emit('save', params);
}
function handleDrawerCancel() {
resetSelector();
}
const showDrawer = computed({
get() {
@ -127,16 +158,20 @@
},
});
const keyword = ref<string>('');
function getFetch() {
setLoadListParams({ keyword: keyword.value });
setLoadListParams({ keyword: keyword.value, projectId: currentProjectId.value, sourceId: props.caseId });
loadList();
}
onMounted(() => {
// getFetch();
});
watch(
() => props.visible,
(val) => {
if (val) {
getFetch();
resetSelector();
}
}
);
</script>
<style scoped></style>

View File

@ -34,19 +34,19 @@
</div>
</div>
<ms-base-table v-if="showType === 'link'" ref="tableRef" v-bind="linkPropsRes" v-on="linkTableEvent">
<template #title="{ record }">
<span class="one-line-text max-w[300px]"> {{ record.title }}</span>
<template #name="{ record }">
<span class="one-line-text max-w-[300px]"> {{ record.name }}</span>
<a-popover title="" position="right">
<span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
<template #content>
<div class="min-w-[300px] text-[14px] text-[var(--color-text-1)]">
{{ record.title }}
<div class="max-w-[600px] text-[14px] text-[var(--color-text-1)]">
{{ record.name }}
</div>
</template>
</a-popover>
</template>
<template #operation="{ record }">
<MsButton @click="cancelLink(record)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
<MsButton @click="cancelLink(record.id)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
</template>
<template v-if="(keyword || '').trim() === ''" #empty>
<div class="flex items-center justify-center">
@ -62,12 +62,19 @@
</template>
</ms-base-table>
<ms-base-table v-else v-bind="testPlanPropsRes" v-on="testPlanTableEvent">
<template #defectName="{ record }">
<span class="one-line-text max-w[300px]"> {{ record.title }}</span
><span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
<template #name="{ record }">
<span class="one-line-text max-w-[300px]"> {{ record.name }}</span>
<a-popover title="" position="right">
<span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
<template #content>
<div class="max-w-[600px] text-[14px] text-[var(--color-text-1)]">
{{ record.name }}
</div>
</template>
</a-popover>
</template>
<template #operation="{ record }">
<MsButton @click="cancelLink(record)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
<MsButton @click="cancelLink(record.id)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
</template>
<template v-if="(keyword || '').trim() === ''" #empty>
<div class="flex items-center justify-center">
@ -79,7 +86,12 @@
</template>
</ms-base-table>
<AddDefectDrawer v-model:visible="showDrawer" />
<LinkDefectDrawer v-model:visible="showLinkDrawer" />
<LinkDefectDrawer
v-model:visible="showLinkDrawer"
:case-id="props.caseId"
:drawer-loading="drawerLoading"
@save="saveHandler"
/>
</div>
</template>
@ -94,18 +106,28 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsRemoveButton from '@/components/business/ms-remove-button/MsRemoveButton.vue';
import AddDefectDrawer from './addDefectDrawer.vue';
import LinkDefectDrawer from './linkDefectDrawer.vue';
import { getBugList } from '@/api/modules/bug-management/index';
import {
associatedDrawerDebug,
cancelAssociatedDebug,
getLinkedCaseBugList,
} from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { characterLimit } from '@/utils';
import { TableKeyEnum } from '@/enums/tableEnum';
import type { TableQueryParams } from '@/models/common';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
caseId: string;
}>();
const showType = ref('link');
const keyword = ref<string>('');
@ -121,8 +143,8 @@
},
{
title: 'caseManagement.featureCase.defectName',
slotName: 'title',
dataIndex: 'title',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: false,
width: 300,
@ -135,7 +157,7 @@
dataIndex: 'defectState',
showInTable: true,
showTooltip: true,
width: 300,
width: 200,
ellipsis: true,
showDrag: false,
},
@ -147,6 +169,15 @@
showTooltip: true,
width: 300,
ellipsis: true,
},
{
title: 'caseManagement.featureCase.defectSource',
slotName: 'defectState',
dataIndex: 'defectState',
showInTable: true,
showTooltip: true,
width: 200,
ellipsis: true,
showDrag: false,
},
{
@ -164,11 +195,11 @@
propsEvent: linkTableEvent,
loadList: loadLinkList,
setLoadListParams: setLinkListParams,
} = useTable(getBugList, {
} = useTable(getLinkedCaseBugList, {
columns,
scroll: { x: '100%' },
scroll: { x: 'auto' },
heightUsed: 340,
enableDrag: true,
enableDrag: false,
});
const testPlanColumns: MsTableColumn = [
@ -183,8 +214,8 @@
},
{
title: 'caseManagement.featureCase.defectName',
slotName: 'defectName',
dataIndex: 'defectName',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: true,
width: 300,
@ -211,15 +242,6 @@
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.tableColumnActions',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 140,
showInTable: true,
showDrag: false,
},
];
const {
@ -227,7 +249,7 @@
propsEvent: testPlanTableEvent,
loadList: testPlanLinkList,
setLoadListParams: setTestPlanListParams,
} = useTable(getBugList, {
} = useTable(getLinkedCaseBugList, {
columns: testPlanColumns,
scroll: { x: '100%' },
heightUsed: 340,
@ -236,15 +258,28 @@
function getFetch() {
if (showType.value === 'link') {
setLinkListParams({ keyword: keyword.value, projectId: appStore.currentProjectId });
setLinkListParams({ keyword: keyword.value, projectId: appStore.currentProjectId, caseId: props.caseId });
loadLinkList();
} else {
setTestPlanListParams({ keyword: keyword.value, projectId: appStore.currentProjectId });
setTestPlanListParams({ keyword: keyword.value, projectId: appStore.currentProjectId, caseId: props.caseId });
testPlanLinkList();
}
}
const cancelLoading = ref<boolean>(false);
//
function cancelLink(record: any) {}
async function cancelLink(id: string) {
cancelLoading.value = true;
try {
if (showType.value === 'link') {
await cancelAssociatedDebug(id);
Message.success(t('caseManagement.featureCase.cancelLinkSuccess'));
}
} catch (error) {
console.log(error);
} finally {
cancelLoading.value = false;
}
}
const showDrawer = ref<boolean>(false);
function createDefect() {
@ -257,6 +292,21 @@
showLinkDrawer.value = true;
}
const drawerLoading = ref<boolean>(false);
async function saveHandler(params: TableQueryParams) {
try {
drawerLoading.value = true;
await associatedDrawerDebug(params);
Message.success(t('caseManagement.featureCase.associatedSuccess'));
getFetch();
showLinkDrawer.value = false;
} catch (error) {
console.log(error);
} finally {
drawerLoading.value = false;
}
}
watch(
() => showType.value,
(val) => {

View File

@ -1,6 +1,6 @@
<template>
<div>
<div class="flex items-center justify-between">
<div class="mb-4 flex items-center justify-between">
<div class="font-medium">{{ t('caseManagement.featureCase.caseReviewList') }}</div>
<a-input-search
v-model:model-value="keyword"

View File

@ -39,7 +39,12 @@
//
async function initCommentList() {
try {
commentList.value = await getCommentList(props.caseId);
const result = await getCommentList(props.caseId);
commentList.value = result.map((item) => {
return {
...item,
};
});
} catch (error) {
console.log(error);
}

View File

@ -5,7 +5,8 @@
:title="t('caseManagement.featureCase.associatedFile')"
:ok-text="t('caseManagement.featureCase.associated')"
:ok-loading="drawerLoading"
:width="960"
:width="1200"
:mask-closable="false"
unmount-on-close
:show-continue="false"
@confirm="handleDrawerConfirm"

View File

@ -1,5 +1,12 @@
<template>
<MsDrawer v-model:visible="innerVisible" :title="title" :width="1200" :footer="false" no-content-padding>
<MsDrawer
v-model:visible="innerVisible"
:title="title"
:width="1200"
:footer="false"
no-content-padding
:mask-closable="false"
>
<div class="flex h-full">
<div class="w-[292px] border-r border-[var(--color-text-n8)] p-[16px]">
<a-input
@ -120,6 +127,7 @@
import {
addPrepositionRelation,
getAssociatedCaseIds,
getCaseModulesCounts,
getCaseModuleTree,
getPrepositionRelation,
@ -279,7 +287,7 @@
(record) => {
return {
...record,
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
tags: (record.tags || []).map((item: string, i: number) => {
return {
id: `${record.id}-${i}`,
name: item,
@ -352,6 +360,7 @@
const searchParams = ref<TableQueryParams>({
projectId: currentProjectId.value,
moduleIds: [],
excludeIds: [],
});
//
@ -373,7 +382,17 @@
focusNodeKey.value = node.id || '';
};
function searchCase() {
async function getAssociatedIds() {
try {
const result = await getAssociatedCaseIds(props.caseId);
searchParams.value.excludeIds = result;
} catch (error) {
console.log(error);
}
}
async function searchCase() {
await getAssociatedIds();
getLoadListParams();
loadList();
getModulesCount();

View File

@ -42,7 +42,11 @@
<div class="flex items-center justify-center">
{{ t('caseManagement.caseReview.tableNoData') }}
<MsButton class="ml-[8px]" @click="addCase">
{{ t('caseManagement.featureCase.addPresetCase') }}
{{
showType === 'preposition'
? t('caseManagement.featureCase.addPresetCase')
: t('caseManagement.featureCase.addPostCase')
}}
</MsButton>
</div>
</template>

View File

@ -245,8 +245,8 @@
* 右侧表格数据刷新后若当前展示的是模块则刷新模块树的统计数量
*/
function initModulesCount(params: CaseModuleQueryParams) {
featureCaseStore.getCaseModulesCountCount(params);
featureCaseStore.getRecycleMModulesCountCount(params);
featureCaseStore.getCaseModulesCount(params);
featureCaseStore.getRecycleModulesCount(params);
tableFilterParams.value = { ...params };
}

View File

@ -245,6 +245,8 @@ export default {
'caseManagement.featureCase.selectTransferDirectory': 'Please select the transfer directory',
'caseManagement.featureCase.quicklyCreateDefectSuccess': 'Quick bug creation success',
'caseManagement.featureCase.cancelDependencyTip': 'Confirm cancel dependencies?',
'caseManagement.featureCase.cancelAssociatedDefectTip': 'Are you sure to cancel the {name} association bug?',
'caseManagement.featureCase.cancelDependencyContent': 'Cancel after impact test plan related statistics',
'caseManagement.featureCase.AssociatedSuccess': 'Associated with success',
'caseManagement.featureCase.associatedSuccess': 'Associated with success',
'caseManagement.featureCase.defectSource': 'defect Source',
};

View File

@ -240,6 +240,8 @@ export default {
'caseManagement.featureCase.selectTransferDirectory': '请选择转存目录',
'caseManagement.featureCase.quicklyCreateDefectSuccess': '快速创建缺陷成功',
'caseManagement.featureCase.cancelDependencyTip': '确认取消依赖关系吗?',
'caseManagement.featureCase.cancelAssociatedDefectTip': '确认取消 {name} 关联缺陷吗?',
'caseManagement.featureCase.cancelDependencyContent': '取消后,影响测试计划相关统计',
'caseManagement.featureCase.AssociatedSuccess': '关联成功',
'caseManagement.featureCase.associatedSuccess': '关联成功',
'caseManagement.featureCase.defectSource': '缺陷来源',
};

View File

@ -56,7 +56,7 @@
<MsButton class="ml-[8px]"> {{ t('project.commonScript.addPublicScript') }} </MsButton>
</div>
</template>
<AddScriptDrawer v-model:visible="showScriptDrawer" />
<AddScriptDrawer v-model:visible="showScriptDrawer" @save="saveHandler" />
<ScriptDetailDrawer v-model:visible="showDetailDrawer" />
</MsCard>
</template>
@ -74,7 +74,9 @@
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import AddScriptDrawer from './components/addScriptDrawer.vue';
// import AddScriptDrawer from './components/addScriptDrawer.vue';
// import ScriptDetailDrawer from './components/scriptDetailDrawer.vue';
import AddScriptDrawer from '@/components/business/ms-common-script/ms-addScriptDrawer.vue';
import ScriptDetailDrawer from './components/scriptDetailDrawer.vue';
import { getDependOnCase } from '@/api/modules/case-management/featureCase';
@ -230,6 +232,9 @@
showDetailDrawer.value = true;
}
//
function saveHandler(isDraft: boolean) {}
function addCommonScript() {
showScriptDrawer.value = true;
}

View File

@ -39,6 +39,18 @@ export default {
'project.commonScript.recover': 'recover',
'project.commonScript.detail': 'detail',
'project.commonScript.changeHistory': 'Change history',
'project.commonScript.apply': 'Apply',
'project.commonScript.insertCommonScript': 'Insert common scripts',
'project.commonScript.commonScriptList': 'Public Script list',
'project.commonScript.folderSearchPlaceholder': 'Please enter a module name',
'project.commonScript.allApis': 'All interface',
'project.commonScript.searchPlaceholder': 'Search by ID or name',
'project.commonScript.noTreeData': 'There is no interface data',
'project.commonScript.apiName': 'Interface name',
'project.commonScript.requestType': 'Request type',
'project.commonScript.responsible': 'Those responsible',
'project.commonScript.path': 'path',
'project.commonScript.saveAsDraft': 'Save as draft',
'code_segment': {
importApiTest: 'Import from API definition',
newApiTest: 'New API test[JSON]',

View File

@ -38,6 +38,18 @@ export default {
'project.commonScript.recover': '恢复',
'project.commonScript.detail': '详情',
'project.commonScript.changeHistory': '变更历史',
'project.commonScript.apply': '应用',
'project.commonScript.insertCommonScript': '插入公共脚本',
'project.commonScript.commonScriptList': '公共脚本列表',
'project.commonScript.folderSearchPlaceholder': '请输入模块名称',
'project.commonScript.allApis': '全部接口',
'project.commonScript.searchPlaceholder': '通过 ID 或名称搜索',
'project.commonScript.noTreeData': '暂无接口数据',
'project.commonScript.apiName': '接口名称',
'project.commonScript.requestType': '请求类型',
'project.commonScript.responsible': '责任人',
'project.commonScript.path': '路径',
'project.commonScript.saveAsDraft': '保存为草稿',
'project': {
code_segment: {
importApiTest: '从API定义导入',