feat(用例评审): 用例评审部分接口&部分组件调整

This commit is contained in:
baiqi 2023-12-08 14:55:03 +08:00 committed by rubylliu
parent 24f12d7b12
commit c6723155a5
28 changed files with 453 additions and 202 deletions

View File

@ -5,10 +5,12 @@ import {
GetAuthDetailUrl,
GetAuthListUrl,
GetBaseInfoUrl,
GetCleanConfigUrl,
GetEmailInfoUrl,
GetPageConfigUrl,
SaveBaseInfoUrl,
SaveBaseUrlUrl,
SaveCleanConfigUrl,
SaveEmailInfoUrl,
SavePageConfigUrl,
TestEmailUrl,
@ -23,6 +25,7 @@ import type {
AuthItem,
AuthParams,
BaseConfig,
CleanupConfig,
EmailConfig,
LDAPConfig,
LDAPConnectConfig,
@ -112,3 +115,13 @@ export function testLdapConnect(data: LDAPConnectConfig) {
export function testLdapLogin(data: LDAPConfig) {
return MSR.post({ url: TestLdapLoginUrl, data });
}
// 保存内存清理配置
export function saveCleanupConfig(data: SaveInfoParams) {
return MSR.post({ url: SaveCleanConfigUrl, data });
}
// 保存内存清理配置
export function getCleanupConfig() {
return MSR.get<CleanupConfig>({ url: GetCleanConfigUrl });
}

View File

@ -30,6 +30,10 @@ export const DeleteAuthUrl = '/system/authsource/delete';
export const TestLdapConnectUrl = '/system/authsource/ldap/test-connect';
// 测试ldap登录
export const TestLdapLoginUrl = '/system/authsource/ldap/test-login';
// 内存清理配置保存
export const SaveCleanConfigUrl = '/system/parameter/edit/clean-config';
// 获取内存清理配置
export const GetCleanConfigUrl = '/system/parameter/get/clean-config';
// 获取系统主页左上角图片
export const GetTitleImgUrl = `${import.meta.env.VITE_API_BASE_URL}/base-display/get/logo-platform`;

View File

@ -19,7 +19,7 @@
<div :class="getFolderClass('all')" @click="setActiveFolder('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name">{{ t('caseManagement.caseReview.allReviews') }}</div>
<div class="folder-count">({{ allFileCount }})</div>
<div class="folder-count">({{ allCaseCount }})</div>
</div>
</div>
<a-divider class="my-[8px]" />
@ -50,12 +50,23 @@
</a-spin>
</div>
<div class="flex w-[calc(100%-293px)] flex-col p-[16px]">
<div class="mb-[16px] 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)]">({{ activeFolderName }})</div>
</div>
<div class="flex items-center gap-[8px]">
<MsAdvanceFilter
v-model:keyword="keyword"
:filter-config-list="filterConfigList"
:row-count="filterRowCount"
:search-placeholder="t('caseManagement.caseReview.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>
<template #right>
<a-select
v-model:model-value="version"
:options="versionOptions"
@ -63,21 +74,9 @@
class="w-[200px]"
allow-clear
/>
<a-input-search
v-model="keyword"
:placeholder="t('ms.case.associate.searchPlaceholder')"
allow-clear
class="w-[200px]"
@press-enter="searchCase"
@search="searchCase"
/>
<a-button type="outline" class="arco-btn-outline--secondary px-[8px]">
<MsIcon type="icon-icon-filter" class="mr-[4px] text-[var(--color-text-4)]" />
<div class="text-[var(--color-text-4)]">{{ t('common.filter') }}</div>
</a-button>
</div>
</div>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
</template>
</MsAdvanceFilter>
<ms-base-table v-bind="propsRes" no-disable class="mt-[16px]" v-on="propsEvent">
<template #caseLevel="{ record }">
<caseLevel :case-level="record.caseLevel" />
</template>
@ -88,9 +87,9 @@
</div>
<div class="flex items-center">
<slot name="footerRight">
<a-button type="secondary" :disabled="loading" class="mr-[12px]" @click="cancel">{{
t('common.cancel')
}}</a-button>
<a-button type="secondary" :disabled="loading" class="mr-[12px]" @click="cancel">
{{ t('common.cancel') }}
</a-button>
<a-button
type="primary"
:loading="loading"
@ -111,6 +110,8 @@
import { computed, onBeforeMount, ref, watch } from 'vue';
import { Message } from '@arco-design/web-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';
@ -121,7 +122,6 @@
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import caseLevel from './caseLevel.vue';
import { getModules } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils';
@ -131,8 +131,10 @@
const props = defineProps<{
visible: boolean;
project: string;
getModulesFunc: (params: any) => Promise<ModuleTreeNode[]>;
modulesCount?: Record<string, number>; //
okButtonDisabled?: boolean; //
selectedKeys?: string[]; // id
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
@ -187,7 +189,9 @@
const activeFolder = ref('all');
const activeFolderName = ref(t('ms.case.associate.allCase'));
const allFileCount = ref(0);
const allCaseCount = ref(0);
const filterRowCount = ref(0);
const filterConfigList = ref<FilterFormItem[]>([]);
function getFolderClass(id: string) {
return activeFolder.value === id ? 'folder-text folder-text--active' : 'folder-text';
@ -213,7 +217,7 @@
async function initModules(isSetDefaultKey = false) {
try {
moduleLoading.value = true;
const res = await getModules(appStore.currentProjectId);
const res = await props.getModulesFunc(appStore.currentProjectId);
folderTree.value = res;
if (isSetDefaultKey) {
selectedModuleKeys.value = [folderTree.value[0].id];
@ -259,7 +263,7 @@
});
/**
* 初始化模块文件数量
* 初始化模块资源数量
*/
watch(
() => props.modulesCount,
@ -327,7 +331,7 @@
isTag: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
() =>
Promise.resolve({
list: [
@ -443,6 +447,7 @@
Message.success(t('ms.case.associate.associateSuccess'));
innerVisible.value = false;
emit('success', Array.from(propsRes.value.selectedKeys));
resetSelector();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -453,6 +458,7 @@
function cancel() {
innerVisible.value = false;
resetSelector();
emit('close');
}

View File

@ -27,7 +27,7 @@
:detail-id="props.detailId"
:detail-index="props.detailIndex"
:table-data="props.tableData"
@loaded="(e) => emit('loaded', e)"
@loaded="handleDetailLoaded"
/>
<div class="ml-auto flex items-center">
<slot name="titleRight" :loading="loading" :detail="detail"></slot>
@ -90,6 +90,11 @@
prevNextButtonRef.value?.openNextDetail();
}
function handleDetailLoaded(val: any) {
detail.value = val;
emit('loaded', val);
}
watch(
() => innerVisible.value,
(val) => {

View File

@ -60,9 +60,17 @@
</a-form>
<MsDescription v-else :descriptions="descriptions">
<template #tag>
<MsTag> 组织 1 </MsTag>
<br />
<MsTag size="small" class="mt-[8px] !bg-[rgb(var(--primary-1))] !text-[rgb(var(--primary-5))]"> 项目 1 </MsTag>
<div v-for="org of orgList" :key="org.orgId" class="mb-[16px]">
<MsTag class="h-[26px]"> {{ org.orgName }} </MsTag>
<br />
<MsTag
v-for="project of org.projectList"
:key="project.projectId"
class="!mr-[8px] mt-[8px] !bg-[rgb(var(--primary-1))] !text-[rgb(var(--primary-5))]"
>
{{ project.projectName }}
</MsTag>
</div>
</template>
</MsDescription>
<a-modal
@ -131,6 +139,8 @@
import useUserStore from '@/store/modules/user/index';
import { validateEmail, validatePhone } from '@/utils/validate';
import { OrganizationProjectListItem } from '@/models/user';
import type { FormInstance } from '@arco-design/web-vue';
const userStore = useUserStore();
@ -139,38 +149,43 @@
const loading = ref(false);
const isEdit = ref(false);
const descriptions = ref<Description[]>([
{
label: t('ms.personal.name'),
value: userStore.name || '',
},
{
label: t('ms.personal.email'),
value: userStore.email || '',
},
{
label: t('ms.personal.phone'),
value: userStore.phone || '',
},
{
label: t('ms.personal.org'),
value: [],
isTag: true,
},
]);
const descriptions = ref<Description[]>([]);
const baseInfoForm = ref({
name: userStore.name,
email: userStore.email,
phone: userStore.phone,
});
const baseInfoFormRef = ref<FormInstance>();
const orgList = ref([]);
const orgList = ref<OrganizationProjectListItem[]>([]);
function initBaseInfo() {
descriptions.value = [
{
label: t('ms.personal.name'),
value: userStore.name || '',
},
{
label: t('ms.personal.email'),
value: userStore.email || '',
},
{
label: t('ms.personal.phone'),
value: userStore.phone || '',
},
{
label: t('ms.personal.org'),
value: [],
isTag: true,
},
];
}
onBeforeMount(async () => {
initBaseInfo();
try {
loading.value = true;
const res = await getBaseInfo(userStore.id || '');
console.log(res);
orgList.value = res.orgProjectList;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -223,6 +238,7 @@
});
Message.success(t('common.updateSuccess'));
await userStore.isLogin();
initBaseInfo();
isEdit.value = false;
} catch (error) {
// eslint-disable-next-line no-console

View File

@ -100,10 +100,17 @@
});
Message.success({
content: t('ms.personal.updatePswSuccess', { count: counting.value }),
duration: 4000,
duration: 1000,
});
setInterval(() => counting.value--, 1000);
const timer = setInterval(() => {
counting.value--;
Message.success({
content: t('ms.personal.updatePswSuccess', { count: counting.value }),
duration: 1000,
});
}, 1000);
setTimeout(() => {
clearInterval(timer);
logout();
}, 3000);
} catch (error) {

View File

@ -45,6 +45,7 @@ export interface MsSearchSelectSlots {
header?: (() => JSX.Element) | Slot<any>;
default?: () => JSX.Element[];
footer?: Slot<any>;
empty?: Slot<any>;
}
export default defineComponent(
@ -283,6 +284,9 @@ export default defineComponent(
if (slots.footer) {
_slots.footer = slots.footer;
}
if (slots.empty) {
_slots.empty = slots.empty;
}
return _slots;
};

View File

@ -5,7 +5,7 @@
v-bind="props"
ref="treeRef"
v-model:expanded-keys="expandedKeys"
:selected-keys="selectedKeys"
v-model:selected-keys="innerSelectedKeys"
:data="treeData"
class="ms-tree"
@drop="onDrop"
@ -355,17 +355,17 @@
}
);
const selectedKeys = ref(props.selectedKeys || []);
const innerSelectedKeys = ref(props.selectedKeys || []);
watch(
() => props.selectedKeys,
(val) => {
selectedKeys.value = val || [];
innerSelectedKeys.value = val || [];
}
);
watch(
() => selectedKeys.value,
() => innerSelectedKeys.value,
(val) => {
emit('update:selectedKeys', val);
}

View File

@ -2,6 +2,7 @@
<div class="flex flex-row justify-between">
<slot name="left"></slot>
<div class="flex flex-row gap-[8px]">
<slot name="right"></slot>
<a-input-search
v-model="innerKeyword"
size="small"
@ -26,7 +27,6 @@
</span>
</span>
</MsTag>
<slot name="right"></slot>
<MsTag no-margin size="large" class="cursor-pointer" theme="outline" @click="handleResetSearch">
<MsIcon class="text-[var(color-text-4)]" :size="16" type="icon-icon_reset_outlined" />
</MsTag>

View File

@ -145,3 +145,8 @@ export interface LDAPConfig extends LDAPConnectConfig {
ldapUserOu: string;
ldapUserMapping: string;
}
// 内存清理配置
export interface CleanupConfig {
operationLog: string;
operationHistory: string;
}

View File

@ -128,8 +128,14 @@ export interface PersonalOrganization {
enable: boolean;
moduleSetting: string;
}
export interface OrganizationProjectMap {
[key: string]: PersonalOrganization[];
export interface PersonalProject {
projectId: string;
projectName: string;
}
export interface OrganizationProjectListItem {
orgId: string;
orgName: string;
projectList: PersonalProject[];
}
export interface PersonalInfo {
id: string;
@ -148,7 +154,7 @@ export interface PersonalInfo {
updateUser: string;
deleted: boolean;
avatar: string;
organizationProjectMap: OrganizationProjectMap;
orgProjectList: OrganizationProjectListItem[];
}
export interface UpdateBaseInfo {
id: string;

View File

@ -3,8 +3,8 @@
v-model:visible="innerVisible"
v-model:project="innerProject"
:ok-button-disabled="associateForm.reviewers.length === 0"
:get-modules-func="getCaseModuleTree"
@success="writeAssociateCases"
@close="emit('close')"
>
<template #footerLeft>
<a-form ref="associateFormRef" :model="associateForm">
@ -45,7 +45,17 @@
allow-clear
multiple
class="w-[300px]"
:loading="reviewerLoading"
>
<template #empty>
<div class="p-[3px_8px] text-[var(--color-text-4)]">
{{ t('caseManagement.caseReview.noMatchReviewer') }}
<span class="cursor-pointer text-[rgb(var(--primary-5))]" @click="goProjectManagement">
{{ t('menu.projectManagement') }}
</span>
<span v-if="currentLocale === 'zh-CN'" class="ml-[4px]">{{ t('common.setting') }}</span>
</div>
</template>
</MsSelect>
</a-form-item>
</a-form>
@ -55,12 +65,16 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { FormInstance } from '@arco-design/web-vue';
import { FormInstance, SelectOptionData } from '@arco-design/web-vue';
import MsCaseAssociate from '@/components/business/ms-case-associate/index.vue';
import MsSelect from '@/components/business/ms-select';
import { getReviewUsers } from '@/api/modules/case-management/caseReview';
import { getCaseModuleTree } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useLocale from '@/locale/useLocale';
import useAppStore from '@/store/modules/app';
import { ProjectManagementRouteEnum } from '@/enums/routeEnum';
@ -76,6 +90,8 @@
}>();
const router = useRouter();
const appStore = useAppStore();
const { currentLocale } = useLocale();
const { t } = useI18n();
const innerVisible = ref(false);
@ -125,24 +141,29 @@
);
}
const reviewersOptions = ref([
{
label: '张三',
value: '1',
},
{
label: '李四',
value: '2',
},
{
label: '王五',
value: '3',
},
]);
const reviewersOptions = ref<SelectOptionData[]>([]);
const reviewerLoading = ref(false);
async function initReviewers() {
try {
reviewerLoading.value = true;
const res = await getReviewUsers(appStore.currentProjectId, '');
reviewersOptions.value = res.map((e) => ({ label: e.name, value: e.id }));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
reviewerLoading.value = false;
}
}
function writeAssociateCases(ids: string[]) {
emit('success', ids);
}
onBeforeMount(() => {
initReviewers();
});
</script>
<style lang="less" scoped></style>

View File

@ -18,7 +18,7 @@
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
<popConfirm mode="add" :all-names="rootModulesName" parent-id="none" @add-finish="initModules">
<popConfirm mode="add" :all-names="rootModulesName" parent-id="NONE" @add-finish="initModules">
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
@ -33,7 +33,7 @@
<a-spin class="min-h-[400px] w-full" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
:selected-keys="selectedKeys"
v-model:selected-keys="selectedKeys"
:data="folderTree"
:keyword="moduleKeyword"
:node-more-actions="folderMoreActions"
@ -83,7 +83,7 @@
:field-config="{ field: renameFolderTitle }"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
@close="resetFocusNodeKey"
@rename-finish="(val) => (nodeData.name = val)"
@rename-finish="initModules"
>
<span :id="`renameSpan${nodeData.id}`" class="relative"></span>
</popConfirm>
@ -139,6 +139,7 @@
const allFileCount = ref(0);
const isExpandAll = ref(props.isExpandAll);
const rootModulesName = ref<string[]>([]); //
const selectedKeys = ref<string[]>([]);
watch(
() => props.isExpandAll,
@ -157,6 +158,9 @@
function setActiveFolder(id: string) {
activeFolder.value = id;
if (id === 'all') {
selectedKeys.value = [];
}
emit('folderNodeSelect', [id], []);
}
@ -182,8 +186,6 @@
];
const renamePopVisible = ref(false);
const selectedKeys = ref<string[]>([]);
/**
* 初始化模块树
* @param isSetDefaultKey 是否设置第一个节点为选中节点
@ -264,7 +266,7 @@
offspringIds.push(e.id);
return e;
});
setActiveFolder(node.id);
emit('folderNodeSelect', _selectedKeys, offspringIds);
}

View File

@ -51,12 +51,7 @@
import { ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import {
addModule,
updateFile,
updateModule,
updateRepository,
} from '@/api/modules/project-management/fileManagement';
import { addReviewModule, updateReviewModule } from '@/api/modules/case-management/caseReview';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -121,13 +116,14 @@
);
function beforeConfirm(done?: (closed: boolean) => void) {
if (loading.value) return;
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
loading.value = true;
if (props.mode === 'add') {
//
await addModule({
await addReviewModule({
projectId: appStore.currentProjectId,
parentId: props.parentId || '',
name: form.value.field,
@ -136,31 +132,7 @@
emit('addFinish', form.value.field);
} else if (props.mode === 'rename') {
//
await updateModule({
id: props.nodeId || '',
name: form.value.field,
});
Message.success(t('project.fileManagement.renameSuccess'));
emit('renameFinish', form.value.field);
} else if (props.mode === 'fileRename') {
//
await updateFile({
id: props.nodeId || '',
name: form.value.field,
});
Message.success(t('project.fileManagement.renameSuccess'));
emit('renameFinish', form.value.field);
} else if (props.mode === 'fileUpdateDesc') {
//
await updateFile({
id: props.nodeId || '',
description: form.value.field,
});
Message.success(t('project.fileManagement.updateDescSuccess'));
emit('updateDescFinish', form.value.field);
} else if (props.mode === 'repositoryRename') {
//
await updateRepository({
await updateReviewModule({
id: props.nodeId || '',
name: form.value.field,
});

View File

@ -7,6 +7,7 @@
:row-count="filterRowCount"
:search-placeholder="t('caseManagement.caseReview.searchPlaceholder')"
@keyword-search="searchReview"
@adv-search="searchReview"
>
<template #left>
<div class="flex items-center">
@ -92,7 +93,7 @@
<template v-if="keyword.trim() === ''" #empty>
<div class="flex items-center justify-center p-[8px] text-[var(--color-text-4)]">
{{ t('caseManagement.caseReview.tableNoData') }}
<MsButton class="ml-[8px]" @click="handleAddClick">
<MsButton class="ml-[8px]" @click="() => emit('goCreate')">
{{ t('caseManagement.caseReview.create') }}
</MsButton>
</div>
@ -213,6 +214,10 @@
moduleTree: ModuleTreeNode[];
}>();
const emit = defineEmits<{
(e: 'goCreate'): void;
}>();
const appStore = useAppStore();
const router = useRouter();
const { t } = useI18n();
@ -491,6 +496,7 @@
setLoadListParams({
keyword: keyword.value,
projectId: appStore.currentProjectId,
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder],
});
loadList();
}
@ -527,7 +533,7 @@
}
/**
* 拦截切换最新版确认
* 删除确认
* @param done 关闭弹窗
*/
async function handleDeleteConfirm(done: (closed: boolean) => void) {
@ -679,9 +685,12 @@
}
}
function handleAddClick() {
console.log('handleAddClick');
}
watch(
() => props.activeFolder,
() => {
searchReview();
}
);
function openDetail(id: string) {
router.push({

View File

@ -85,6 +85,7 @@
:search-keys="['label']"
allow-search
multiple
:loading="reviewerLoading"
/>
</a-form-item>
<a-form-item field="tags" :label="t('caseManagement.caseReview.tag')">
@ -138,80 +139,38 @@
</div>
</template>
</MsCard>
<MsCaseAssociate
<AssociateDrawer
v-model:visible="caseAssociateVisible"
v-model:project="caseAssociateProject"
:ok-button-disabled="associateForm.reviewers.length === 0"
@success="writeAssociateCases"
>
<template #footerLeft>
<a-form ref="associateFormRef" :model="associateForm">
<a-form-item
field="reviewers"
:rules="[{ required: true, message: t('caseManagement.caseReview.reviewerRequired') }]"
class="mb-0"
>
<template #label>
<div class="inline-flex items-center">
{{ t('caseManagement.caseReview.reviewer') }}
<a-tooltip position="right">
<template #content>
<div>{{ t('caseManagement.caseReview.switchProject') }}</div>
<div>{{ t('caseManagement.caseReview.resetReviews') }}</div>
<div>
{{ t('caseManagement.caseReview.reviewsTip') }}
<span class="cursor-pointer text-[rgb(var(--primary-4))]" @click="goProjectManagement">
{{ t('menu.projectManagement') }}
</span>
{{ t('caseManagement.caseReview.reviewsTip2') }}
</div>
</template>
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</template>
<MsSelect
v-model:modelValue="associateForm.reviewers"
mode="static"
:placeholder="t('caseManagement.caseReview.reviewerPlaceholder')"
:options="reviewersOptions"
:search-keys="['label']"
allow-search
allow-clear
multiple
class="w-[300px]"
>
</MsSelect>
</a-form-item>
</a-form>
</template>
</MsCaseAssociate>
/>
</template>
<script setup lang="ts">
/**
* @description 功能测试-用例评审-创建评审
*/
import { onBeforeMount } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { Message, SelectOptionData } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import MsCaseAssociate from '@/components/business/ms-case-associate/index.vue';
import MsSelect from '@/components/business/ms-select';
import AssociateDrawer from './components/create/associateDrawer.vue';
import { getReviewUsers } from '@/api/modules/case-management/caseReview';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { CaseManagementRouteEnum, ProjectManagementRouteEnum } from '@/enums/routeEnum';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import type { FormInstance } from '@arco-design/web-vue';
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const { t } = useI18n();
const isEdit = ref(!!route.query.id);
@ -239,20 +198,21 @@
value: '2',
},
]);
const reviewersOptions = ref([
{
label: '张三',
value: '1',
},
{
label: '李四',
value: '2',
},
{
label: '王五',
value: '3',
},
]);
const reviewersOptions = ref<SelectOptionData[]>([]);
const reviewerLoading = ref(false);
async function initReviewers() {
try {
reviewerLoading.value = true;
const res = await getReviewUsers(appStore.currentProjectId, '');
reviewersOptions.value = res.map((e) => ({ label: e.name, value: e.id }));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
reviewerLoading.value = false;
}
}
const selectedAssociateCases = ref<string[]>([]);
@ -299,18 +259,10 @@
const caseAssociateVisible = ref<boolean>(false);
const caseAssociateProject = ref('');
const associateForm = ref({
reviewers: [],
});
const associateFormRef = ref<FormInstance>();
function goProjectManagement() {
window.open(
`${window.location.origin}#${
router.resolve({ name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_PERMISSION_USER_GROUP }).fullPath
}`
);
}
onBeforeMount(() => {
initReviewers();
});
</script>
<style lang="less" scoped>

View File

@ -16,7 +16,7 @@
</div>
</template>
<template #right>
<ReviewTable :active-folder="activeFolderId" :module-tree="moduleTree" />
<ReviewTable :active-folder="activeFolderId" :module-tree="moduleTree" @go-create="goCreateReview" />
</template>
</MsSplitBox>
</div>

View File

@ -78,4 +78,55 @@ export default {
'caseManagement.caseReview.reviewsTip2': 'can set permissions',
'caseManagement.caseReview.clearSelectedCases': 'Clear selected use cases',
'caseManagement.caseReview.selectedCases': '{count} use cases selected',
'caseManagement.caseReview.onlyMine': 'See only Mine',
'caseManagement.caseReview.createTestPlan': 'Create test plan',
'caseManagement.caseReview.reviewedCase': 'Reviewed cases',
'caseManagement.caseReview.createCase': 'Create cases',
'caseManagement.caseReview.allCases': 'All cases',
'caseManagement.caseReview.noCases': 'No matching case data yet',
'caseManagement.caseReview.caseName': 'Case name',
'caseManagement.caseReview.reviewResult': 'Review results',
'caseManagement.caseReview.reviewResultTip':
'When "See only mine" is turned on, you can view my review results on the list',
'caseManagement.caseReview.disassociate': 'Disassociate',
'caseManagement.caseReview.disassociateConfirmTitle':
'Are you sure you want to cancel the selected {count} test cases?',
'caseManagement.caseReview.version': 'Version',
'caseManagement.caseReview.unReview': 'Unreviewed',
'caseManagement.caseReview.reviewPass': 'Review passed',
'caseManagement.caseReview.disassociateTip': 'Are you sure to cancel the association?',
'caseManagement.caseReview.disassociateTipContent':
'After cancellation, associate again, the review result is: Unreviewed',
'caseManagement.caseReview.changeReviewer': 'Modify reviewer',
'caseManagement.caseReview.batchReview': 'Batch review',
'caseManagement.caseReview.selectedCase': '{count} use cases selected',
'caseManagement.caseReview.reason': 'Reason',
'caseManagement.caseReview.reasonRequired': 'Reason cannot be empty',
'caseManagement.caseReview.reasonPlaceholder': 'Please enter the reason',
'caseManagement.caseReview.commitResult': 'Submit results',
'caseManagement.caseReview.batchReviewTip':
'Modifying the review results means modifying the personal review results.',
'caseManagement.caseReview.chooseReviewer': 'Select reviewers',
'caseManagement.caseReview.batchChangeReviewer': 'Batch modify reviewers',
'caseManagement.caseReview.append': 'Append',
'caseManagement.caseReview.appendTip1': 'Open: Add reviewer',
'caseManagement.caseReview.appendTip2': 'Close: Update reviewers',
'caseManagement.caseReview.myReview': 'My reviews',
'caseManagement.caseReview.caseLevel': 'Case level',
'caseManagement.caseReview.caseVersion': 'Case version',
'caseManagement.caseReview.caseStatus': 'Case status',
'caseManagement.caseReview.responsiblePerson': 'Responsible person',
'caseManagement.caseReview.createTime': 'Created time',
'caseManagement.caseReview.caseBaseInfo': 'Basic info',
'caseManagement.caseReview.caseDetail': 'Detail',
'caseManagement.caseReview.caseDemand': 'Demand',
'caseManagement.caseReview.startReview': 'Start review',
'caseManagement.caseReview.autoNext': 'Automatically next',
'caseManagement.caseReview.autoNextTip1': 'Open: After submitting the review results, jump to the next use case',
'caseManagement.caseReview.autoNextTip2': 'Close: After submitting the review results, it is still current',
'caseManagement.caseReview.suggestion': 'Suggestion',
'caseManagement.caseReview.suggestionTip': 'Not as a result of the review',
'caseManagement.caseReview.submitReview': 'Submit review',
'caseManagement.caseReview.reviewHistory': 'Review history',
'caseManagement.caseReview.noMatchReviewer': 'No matching handler, can be set in {menu}',
};

View File

@ -118,4 +118,5 @@ export default {
'caseManagement.caseReview.suggestionTip': '不作为评审结果',
'caseManagement.caseReview.submitReview': '提交评审',
'caseManagement.caseReview.reviewHistory': '评审历史',
'caseManagement.caseReview.noMatchReviewer': '无匹配处理人,可在 ',
};

View File

@ -57,7 +57,7 @@
:field-config="{ field: renameFolderTitle }"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
@close="resetFocusNodeKey"
@rename-finish="(val) => (nodeData.name = val)"
@rename-finish="() => initModules()"
>
<span :id="`renameSpan${nodeData.id}`" class="relative"></span>
</popConfirm>

View File

@ -121,6 +121,7 @@
);
function beforeConfirm(done?: (closed: boolean) => void) {
if (loading.value) return;
formRef.value?.validate(async (errors) => {
if (!errors) {
try {

View File

@ -0,0 +1,141 @@
<template>
<MsCard class="mb-[16px]" :loading="loading" simple auto-height>
<div class="mb-[16px] flex justify-between">
<div class="font-medium text-[var(--color-text-000)]">{{ t('system.config.memoryCleanup') }}</div>
</div>
<a-radio-group v-model:model-value="activeType" type="button">
<a-radio value="log">{{ t('system.config.memoryCleanup.log') }}</a-radio>
<a-radio value="history">{{ t('system.config.memoryCleanup.history') }}</a-radio>
</a-radio-group>
<template v-if="activeType === 'log'">
<div class="mb-[8px] mt-[16px] flex items-center">
<div class="text-[var(--color-text-000)]">{{ t('system.config.memoryCleanup.keepTime') }}</div>
<a-tooltip :content="t('system.config.memoryCleanup.keepTimeTip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
<a-input-number
v-model:model-value="timeCount"
class="w-[130px]"
:disabled="saveLoading"
@blur="() => saveConfig()"
>
<template #append>
<a-select
v-model:model-value="activeTime"
:options="timeOptions"
class="time-input-append"
:loading="saveLoading"
@change="() => saveConfig()"
/>
</template>
</a-input-number>
</template>
<template v-else>
<div class="mb-[8px] mt-[16px] flex items-center">
<div class="text-[var(--color-text-000)]">{{ t('system.config.memoryCleanup.saveCount') }}</div>
<a-tooltip :content="t('system.config.memoryCleanup.saveCountTip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
<a-input-number
v-model:model-value="historyCount"
class="w-[130px]"
:disabled="saveLoading"
@blur="() => saveConfig()"
/>
</template>
</MsCard>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import { getCleanupConfig, saveCleanupConfig } from '@/api/modules/setting/config';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const loading = ref(false);
const activeType = ref('log');
const timeCount = ref(6);
const activeTime = ref('M');
const timeOptions = [
{
label: t('system.config.memoryCleanup.day'),
value: 'D',
},
{
label: t('system.config.memoryCleanup.month'),
value: 'M',
},
{
label: t('system.config.memoryCleanup.year'),
value: 'Y',
},
];
const historyCount = ref(10);
onBeforeMount(async () => {
loading.value = true;
const res = await getCleanupConfig();
if (res.operationLog) {
const matches = res.operationLog.match(/(\d+)([MDY])$/);
if (matches) {
const [, number, letter] = matches;
timeCount.value = Number(number);
activeTime.value = letter;
}
}
if (res.operationHistory) {
historyCount.value = Number(res.operationHistory);
}
loading.value = false;
});
const saveLoading = ref(false);
async function saveConfig() {
saveLoading.value = true;
await saveCleanupConfig([
{
paramKey: 'cleanConfig.operation.log',
paramValue: `${timeCount.value}${activeTime.value}`,
type: 'string',
},
{
paramKey: 'cleanConfig.operation.history',
paramValue: historyCount.value.toString(),
type: 'string',
},
]);
saveLoading.value = false;
Message.success(t('system.config.memoryCleanup.setSuccess'));
}
</script>
<style lang="less" scoped>
:deep(.arco-input-append) {
@apply border-none;
}
:deep(.time-input-append) {
@apply z-10;
margin-left: -16px !important;
border-radius: 0 4px 4px 0 !important;
background-color: var(--color-text-n8) !important;
&:hover {
border-color: rgb(var(--primary-5)) !important;
background-color: var(--color-text-n8) !important;
}
}
</style>

View File

@ -3,6 +3,7 @@
<baseConfig v-show="activeTab === 'baseConfig'" />
<pageConfig v-if="isInitPageConfig" v-show="activeTab === 'pageConfig'" />
<authConfig v-if="isInitAuthConfig" v-show="activeTab === 'authConfig'" ref="authConfigRef" />
<memoryCleanup v-if="isInitMemoryCleanup" v-show="activeTab === 'memoryCleanup'" />
</template>
<script setup lang="ts">
@ -15,6 +16,7 @@
import MsTabCard from '@/components/pure/ms-tab-card/index.vue';
import authConfig, { AuthConfigInstance } from './components/authConfig.vue';
import baseConfig from './components/baseConfig.vue';
import memoryCleanup from './components/memoryCleanup.vue';
import pageConfig from './components/pageConfig.vue';
import { useI18n } from '@/hooks/useI18n';
@ -25,11 +27,13 @@
const activeTab = ref((route.query.tab as string) || 'baseConfig');
const isInitPageConfig = ref(activeTab.value === 'pageConfig');
const isInitAuthConfig = ref(activeTab.value === 'authConfig');
const isInitMemoryCleanup = ref(activeTab.value === 'memoryCleanup');
const authConfigRef = ref<AuthConfigInstance | null>();
const tabList = [
{ key: 'baseConfig', title: t('system.config.baseConfig') },
{ key: 'pageConfig', title: t('system.config.pageConfig') },
{ key: 'authConfig', title: t('system.config.authConfig') },
{ key: 'memoryCleanup', title: t('system.config.memoryCleanup') },
];
watch(
@ -39,6 +43,8 @@
isInitPageConfig.value = true;
} else if (val === 'authConfig' && !isInitAuthConfig.value) {
isInitAuthConfig.value = true;
} else if (val === 'memoryCleanup' && !isInitMemoryCleanup.value) {
isInitMemoryCleanup.value = true;
}
},
{

View File

@ -207,4 +207,16 @@ export default {
'system.config.auth.testLoginPasswordNotNull': 'LDAP login password cannot be empty',
'system.config.auth.testLoginSuccess': 'LDAP login successful',
'system.config.auth.testLoginCancel': 'Cancel',
'system.config.memoryCleanup': 'Memory cleanup',
'system.config.memoryCleanup.log': 'Log',
'system.config.memoryCleanup.history': 'Change history',
'system.config.memoryCleanup.keepTime': 'Retention time',
'system.config.memoryCleanup.keepTimeTip': 'The system will perform cleanup at 00:00 every day',
'system.config.memoryCleanup.day': 'Day',
'system.config.memoryCleanup.month': 'Month',
'system.config.memoryCleanup.year': 'Year',
'system.config.memoryCleanup.setSuccess': 'Setup successful',
'system.config.memoryCleanup.saveCount': 'Reserved quantity',
'system.config.memoryCleanup.saveCountTip':
'It is effective for all projects in the system. Use case change history that exceeds the settings will be cleared and will take effect immediately after the update.',
};

View File

@ -202,4 +202,15 @@ export default {
'system.config.auth.testLoginPasswordNotNull': 'LDAP 登录密码不能为空',
'system.config.auth.testLoginCancel': '取消测试',
'system.config.auth.testLoginSuccess': 'LDAP 登录成功',
'system.config.memoryCleanup': '内存清理',
'system.config.memoryCleanup.log': '日志',
'system.config.memoryCleanup.history': '变更历史',
'system.config.memoryCleanup.keepTime': '保留时长',
'system.config.memoryCleanup.keepTimeTip': '系统会在每天 00:00 执行清理',
'system.config.memoryCleanup.day': '天',
'system.config.memoryCleanup.month': '月',
'system.config.memoryCleanup.year': '年',
'system.config.memoryCleanup.setSuccess': '设置成功',
'system.config.memoryCleanup.saveCount': '保留条数',
'system.config.memoryCleanup.saveCountTip': '对系统内所有的项目生效,超出设置的用例变更历史会被清除,更新后立即生效',
};

View File

@ -400,6 +400,10 @@
label: 'system.log.operateType.logout',
value: 'LOGOUT',
},
{
label: 'system.log.operateType.associate',
value: 'ASSOCIATE',
},
{
label: 'system.log.operateType.disassociate',
value: 'DISASSOCIATE',

View File

@ -30,6 +30,7 @@ export default {
'system.log.operateType.recover': 'Recover',
'system.log.operateType.logout': 'Logout',
'system.log.operateType.disassociate': 'Disassociate',
'system.log.operateType.associate': 'Associate',
'system.log.operateType.archived': 'Archived',
'system.log.log': 'Operation log',
'system.log.time': 'Operation time',

View File

@ -30,6 +30,7 @@ export default {
'system.log.operateType.recover': '恢复',
'system.log.operateType.logout': '登出',
'system.log.operateType.disassociate': '取消关联',
'system.log.operateType.associate': '关联',
'system.log.operateType.archived': '归档',
'system.log.log': '操作日志',
'system.log.time': '操作时间',