feat(项目管理): 公共脚本页面&富文本内容回显调整

This commit is contained in:
xinxin.wu 2024-01-08 15:18:29 +08:00 committed by Craftsman
parent d086de31a6
commit e073d53d6e
34 changed files with 816 additions and 73 deletions

View File

@ -9,6 +9,7 @@ import {
BatchEditCaseUrl,
BatchMoveCaseUrl,
CancelAssociationDemandUrl,
cancelPreAndPostCaseUrl,
checkFileIsUpdateUrl,
CreateCaseModuleTreeUrl,
CreateCaseUrl,
@ -309,4 +310,8 @@ export function getPrepositionRelation(data: TableQueryParams) {
export function addPrepositionRelation(data: TableQueryParams) {
return MSR.post<ModulesTreeType[]>({ url: `${AddDependOnRelationUrl}`, data });
}
// 取消依赖关系
export function cancelPreOrPostCase(id: string) {
return MSR.get({ url: `${cancelPreAndPostCaseUrl}/${id}` });
}
export default {};

View File

@ -114,3 +114,5 @@ export const GetDependOnPageUrl = '/functional/case/relationship/page';
export const GetDependOnRelationUrl = '/functional/case/relationship/relate/page';
// 添加前后置关系
export const AddDependOnRelationUrl = '/functional/case/relationship/add';
// 取消关联前后置关系
export const cancelPreAndPostCaseUrl = '/functional/case/relationship/delete';

View File

@ -10,7 +10,7 @@
<div class="w-full items-center">
<a-input v-if="!isActive" class="w-full" @click="isActive = true"></a-input>
<div v-else class="flex flex-col justify-between">
<MsRichText v-model="currentContent" class="w-full" />
<MsRichText v-model:raw="currentContent" class="w-full" />
<div class="mt-4 flex flex-row justify-end gap-[12px]">
<a-button @click="cancelClick">{{ t('common.cancel') }}</a-button>
<a-button type="primary" :disabled="!currentContent" @click="publish">{{ t('common.publish') }}</a-button>

View File

@ -5,10 +5,11 @@
:sub-title-tip="props.subTitleTip"
:loading="props.loading"
:visible="currentVisible"
:ok-text="props.okText"
@confirm="handleOk"
@cancel="handleCancel"
>
<MsButton @click="showPopover">{{ t('common.remove') }}</MsButton>
<MsButton @click="showPopover">{{ t(props.removeText) }}</MsButton>
</MsPopconfirm>
</template>
@ -20,11 +21,18 @@
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
title: string;
subTitleTip: string;
loading?: boolean;
}>();
const props = withDefaults(
defineProps<{
title: string;
subTitleTip: string;
loading?: boolean;
removeText?: string;
okText?: string;
}>(),
{
removeText: 'common.remove',
}
);
const emit = defineEmits<{
(e: 'ok'): void;

View File

@ -2,7 +2,7 @@
/**
*
* @name: MsRichText.vue
* @param {string} modelValue v-model绑定的值
* @param {string} raw v-model绑定的值
* @return {string} 返回编辑器内容
* @description: 富文本编辑器
* @example:
@ -12,11 +12,14 @@
* import rehypeStringify from 'rehype-stringify';
* return unified().use(rehypeParse).use(rehypeFormat).use(rehypeStringify).processSync(content.value);
*/
import { useDebounceFn, useLocalStorage } from '@vueuse/core';
import useLocale from '@/locale/useLocale';
import '@halo-dev/richtext-editor/dist/style.css';
import suggestion from './extensions/mention/suggestion';
import {
Editor,
ExtensionAudio,
ExtensionBlockquote,
ExtensionBold,
@ -57,19 +60,36 @@
ExtensionVideo,
lowlight,
RichTextEditor,
useEditor,
} from '@halo-dev/richtext-editor';
import Mention from '@tiptap/extension-mention';
const props = defineProps<{
modelValue: string;
const props = withDefaults(
defineProps<{
raw?: string;
uploadImage?: (file: File) => Promise<any>;
}>(),
{
raw: '',
uploadImage: undefined,
}
);
const editor = shallowRef<Editor>();
const emit = defineEmits<{
(event: 'update:raw', value: string): void;
(event: 'update', value: string): void;
}>();
const emit = defineEmits(['update:model-value']);
const content = ref('');
// debounce OnUpdate
const debounceOnUpdate = useDebounceFn(() => {
const html = `${editor.value?.getHTML()}`;
emit('update:raw', html);
emit('update', html);
}, 250);
const editor = useEditor({
content: content.value,
editor.value = new Editor({
content: props.raw,
extensions: [
ExtensionBlockquote,
ExtensionBold,
@ -92,6 +112,8 @@
ExtensionStrike,
ExtensionText,
ExtensionImage.configure({
inline: true,
allowBase64: false,
HTMLAttributes: {
loading: 'lazy',
},
@ -132,31 +154,38 @@
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: () => {
content.value = `${editor.value?.getHTML()}`;
debounceOnUpdate();
},
});
const { currentLocale } = useLocale();
const locale = computed(() => currentLocale.value as 'zh-CN' | 'en-US');
watch(
() => props.modelValue,
(val) => {
if (val) {
content.value = val;
() => props.raw,
() => {
if (props.raw !== editor.value?.getHTML()) {
editor.value?.commands.setContent(props.raw);
}
},
{
immediate: true,
}
);
watch(
() => content.value,
() => {
emit('update:model-value', `${editor.value?.getHTML()}`);
}
);
onBeforeUnmount(() => {
editor.value?.destroy();
});
</script>
<template>

View File

@ -44,7 +44,7 @@
const item = props.items[index];
if (item) {
props.command({ id: `${item.name}(${item.email})` } as any);
props.command({ id: item.id, label: `${item.name}` } as any);
}
}
@ -73,7 +73,7 @@
function selectItem(index: any) {
const item = props.items[index];
if (item) {
props.command({ id: `${item.name}(${item.email})` } as any);
props.command({ id: item.id, label: `${item.name}` } as any);
}
}

View File

@ -5,7 +5,8 @@ import useAppStore from '@/store/modules/app';
import type { UserListItem } from '@/models/setting/user';
import { VueRenderer } from '@halo-dev/richtext-editor';
import { Extension, VueRenderer } from '@halo-dev/richtext-editor';
import Suggestion from '@tiptap/suggestion';
import type { Instance } from 'tippy.js';
import tippy from 'tippy.js';

View File

@ -30,6 +30,7 @@ export enum ProjectManagementRouteEnum {
PROJECT_MANAGEMENT = 'projectManagement',
PROJECT_MANAGEMENT_FILE_MANAGEMENT = 'projectManagementFileManageMent',
PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT = 'projectManagementMessageManagement',
PROJECT_MANAGEMENT_COMMON_SCRIPT = 'projectManagementCommonScript',
PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_EDIT = 'projectManagementMessageManagementEdit',
PROJECT_MANAGEMENT_LOG = 'projectManagementLog',
PROJECT_MANAGEMENT_PERMISSION = 'projectManagementPermission',

View File

@ -23,6 +23,7 @@ export enum TableKeyEnum {
ORGANIZATION_TEMPLATE_MANAGEMENT_STEP = 'organizationTemplateManagementStep',
ORGANIZATION_PROJECT = 'organizationProject',
ORGANIZATION_PROJECT_USER_DRAWER = 'organizationProjectUserDrawer',
ORGANIZATION_PROJECT_COMMON_SCRIPT = 'projectManagementCommonScript',
FILE_MANAGEMENT_FILE = 'fileManagementFile',
FILE_MANAGEMENT_CASE = 'fileManagementCase',
FILE_MANAGEMENT_CASE_RECYCLE = 'fileManagementCaseRecycle',

View File

@ -30,6 +30,7 @@ export default {
'menu.projectManagement': 'Project',
'menu.projectManagement.fileManagement': 'File Management',
'menu.projectManagement.messageManagement': 'Message Management',
'menu.projectManagement.commonScript': 'Common Script',
'menu.projectManagement.messageManagementEdit': 'Update Template',
'menu.caseManagement.featureCase': 'Feature Case',
'menu.caseManagement.featureCaseRecycle': 'Recycle',

View File

@ -33,6 +33,7 @@ export default {
'menu.projectManagement.log': '日志',
'menu.projectManagement.fileManagement': '文件管理',
'menu.projectManagement.messageManagement': '消息管理',
'menu.projectManagement.commonScript': '公共脚本',
'menu.projectManagement.messageManagementEdit': '更新模板',
'menu.caseManagement.featureCase': '功能用例',
'menu.caseManagement.featureCaseRecycle': '回收站',

View File

@ -0,0 +1,5 @@
export interface CommonScriptMenu {
title: string;
value: string;
command?: string;
}

View File

@ -220,6 +220,17 @@ const ProjectManagement: AppRouteRecordRaw = {
],
},
},
// 公共脚本
{
path: 'commonScript',
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_COMMON_SCRIPT,
component: () => import('@/views/project-management/commonScript/index.vue'),
meta: {
locale: 'menu.projectManagement.commonScript',
roles: ['*'],
isTopMenu: true,
},
},
// 项目日志
{
path: 'log',

View File

@ -15,7 +15,7 @@
<div class="mt-[8]" :class="{ 'max-h-[260px]': contentEditAble }">
<MsRichText
v-if="form.content"
v-model:model-value="form.content"
v-model:raw="form.content"
:disabled="!contentEditAble"
:placeholder="t('bugManagement.edit.contentPlaceholder')"
/>

View File

@ -32,7 +32,7 @@
<a-input v-model="form.title" :max-length="255" show-word-limit />
</a-form-item>
<a-form-item field="description" :label="t('bugManagement.edit.content')">
<MsRichText v-model="form.description" />
<MsRichText v-model:raw="form.description" />
</a-form-item>
<a-form-item field="attachment" :label="t('bugManagement.edit.file')">
<div class="flex flex-col">

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:model-value="form.prerequisite" />
<MsRichText v-model:raw="form.prerequisite" />
</a-form-item>
<a-form-item
field="step"
@ -48,17 +48,17 @@
<AddStep v-model:step-list="stepData" :is-disabled="false" />
</div>
<!-- 文本描述 -->
<MsRichText v-else v-model:modelValue="form.textDescription" />
<MsRichText v-else v-model:raw="form.textDescription" />
</a-form-item>
<a-form-item
v-if="form.caseEditType === 'TEXT'"
field="remark"
:label="t('caseManagement.featureCase.expectedResult')"
>
<MsRichText v-model:modelValue="form.expectedResult" />
<MsRichText v-model:raw="form.expectedResult" />
</a-form-item>
<a-form-item field="remark" :label="t('caseManagement.featureCase.remark')">
<MsRichText v-model:modelValue="form.description" />
<MsRichText v-model:raw="form.description" />
</a-form-item>
<AddAttachment @change="handleChange" @link-file="associatedFile" @upload="beforeUpload" />
</a-form>

View File

@ -22,7 +22,7 @@
<a-input v-model="form.title" :max-length="255" show-word-limit />
</a-form-item>
<a-form-item :label="t('bugManagement.edit.content')">
<MsRichText v-model="form.description" />
<MsRichText v-model:raw="form.description" />
</a-form-item>
</a-form>
</MsDrawer>

View File

@ -144,7 +144,7 @@
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'success', val: string[]): void;
(e: 'success'): void;
(e: 'close'): void;
}>();
@ -273,6 +273,7 @@
},
showSetting: false,
selectable: true,
heightUsed: 300,
showSelectAll: true,
},
(record) => {
@ -332,21 +333,16 @@
moduleLoading.value = false;
}
}
/**
* @param 获取回收站模块
*/
// count
const emitTableParams: CaseModuleQueryParams = {
keyword: keyword.value,
moduleIds: [],
projectId: currentProjectId.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
};
async function getModulesCount() {
try {
const emitTableParams: CaseModuleQueryParams = {
keyword: keyword.value,
moduleIds: [],
projectId: currentProjectId.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
};
modulesCount.value = await getCaseModulesCounts(emitTableParams);
} catch (error) {
console.log(error);
@ -401,6 +397,7 @@
};
await addPrepositionRelation(params);
Message.success(t('common.addSuccess'));
emit('success');
innerVisible.value = false;
resetSelector();
} catch (error) {

View File

@ -2,10 +2,10 @@
<div>
<div class="flex items-center justify-between">
<div>
<a-button v-if="showType === 'preposition'" class="mr-3" type="primary" @click="addCase('preposition')">
<a-button v-if="showType === 'preposition'" class="mr-3" type="primary" @click="addCase">
{{ t('caseManagement.featureCase.addPresetCase') }}
</a-button>
<a-button v-else type="primary" @click="addCase('postPosition')">
<a-button v-else type="primary" @click="addCase">
{{ t('caseManagement.featureCase.addPostCase') }}
</a-button>
</div>
@ -28,33 +28,50 @@
</div>
<ms-base-table ref="tableRef" v-bind="propsRes" v-on="propsEvent">
<template #operation="{ record }">
<MsButton @click="cancelDependency(record)">{{ t('caseManagement.featureCase.cancelDependency') }}</MsButton>
<MsRemoveButton
position="br"
ok-text="common.confirm"
remove-text="caseManagement.featureCase.cancelDependency"
:title="t('caseManagement.featureCase.cancelDependencyTip', { name: characterLimit(record.name) })"
:sub-title-tip="t('caseManagement.featureCase.cancelDependencyContent')"
:loading="cancelLoading"
@ok="cancelDependency(record)"
/>
</template>
<template v-if="(keyword || '').trim() === ''" #empty>
<div class="flex items-center justify-center">
{{ t('caseManagement.caseReview.tableNoData') }}
<MsButton class="ml-[8px]" @click="addCase('preposition')">
<MsButton class="ml-[8px]" @click="addCase">
{{ t('caseManagement.featureCase.addPresetCase') }}
</MsButton>
</div>
</template>
</ms-base-table>
<PreAndPostCaseDrawer ref="drawerRef" v-model:visible="showDrawer" :show-type="showType" :case-id="props.caseId" />
<PreAndPostCaseDrawer
ref="drawerRef"
v-model:visible="showDrawer"
:show-type="showType"
:case-id="props.caseId"
@success="successHandler"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/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 MsRemoveButton from '@/components/business/ms-remove-button/MsRemoveButton.vue';
import PreAndPostCaseDrawer from './preAndPostCaseDrawer.vue';
import { getDependOnCase } from '@/api/modules/case-management/featureCase';
import { cancelPreOrPostCase, getDependOnCase } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { characterLimit } from '@/utils';
const appStore = useAppStore();
@ -88,8 +105,8 @@
},
{
title: 'caseManagement.featureCase.tableColumnVersion',
slotName: 'defectState',
dataIndex: 'defectState',
slotName: 'versionName',
dataIndex: 'versionName',
showInTable: true,
showTooltip: true,
width: 300,
@ -97,8 +114,8 @@
},
{
title: 'caseManagement.featureCase.tableColumnCreateUser',
slotName: 'createUser',
dataIndex: 'createUser',
slotName: 'userName',
dataIndex: 'userName',
showInTable: true,
showTooltip: true,
width: 300,
@ -124,8 +141,20 @@
enableDrag: true,
});
const cancelLoading = ref<boolean>(false);
//
function cancelDependency(record: any) {}
async function cancelDependency(record: any) {
cancelLoading.value = true;
try {
await cancelPreOrPostCase(record.id);
Message.success(t('caseManagement.featureCase.cancelFollowSuccess'));
loadList();
} catch (error) {
console.log(error);
} finally {
cancelLoading.value = false;
}
}
function getParams() {
setLoadListParams({
@ -139,11 +168,15 @@
const showDrawer = ref<boolean>(false);
const drawerRef = ref();
//
function addCase(type: string) {
function addCase() {
showDrawer.value = true;
drawerRef.value.initModules();
}
function successHandler() {
loadList();
}
watch(
() => showType.value,
() => {

View File

@ -15,8 +15,8 @@
}}</a-button
></span
>
<MsRichText v-if="isEditPreposition" v-model:model-value="detailForm.prerequisite" class="mt-2" />
<div v-else class="text-[var(--color-text-3)]" v-html="detailForm?.prerequisite || '-'"></div>
<MsRichText v-if="isEditPreposition" v-model:raw="detailForm.prerequisite" class="mt-2" />
<div v-else v-dompurify-html="detailForm?.prerequisite || '-'" class="text-[var(--color-text-3)]"></div>
</a-form-item>
<a-form-item
field="step"
@ -50,7 +50,7 @@
<!-- 文本描述 -->
<MsRichText
v-if="detailForm.caseEditType === 'TEXT' && isEditPreposition"
v-model:modelValue="detailForm.textDescription"
v-model:raw="detailForm.textDescription"
/>
<div v-if="detailForm.caseEditType === 'TEXT' && !isEditPreposition">{{
detailForm.textDescription || '-'
@ -63,13 +63,13 @@
>
<MsRichText
v-if="detailForm.caseEditType === 'TEXT' && isEditPreposition"
v-model:modelValue="detailForm.expectedResult"
v-model:raw="detailForm.expectedResult"
/>
<div v-else class="text-[var(--color-text-3)]" v-html="detailForm.description || '-'"></div>
</a-form-item>
<a-form-item field="remark" :label="t('caseManagement.featureCase.remark')">
<MsRichText v-if="isEditPreposition" v-model:modelValue="detailForm.description" />
<div v-else class="text-[var(--color-text-3)]" v-html="detailForm.description || '-'"></div>
<MsRichText v-if="isEditPreposition" v-model:raw="detailForm.description" />
<div v-else v-dompurify-html="detailForm.description || '-'" class="text-[var(--color-text-3)]"></div>
</a-form-item>
<div v-if="isEditPreposition" class="flex justify-end">
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>

View File

@ -244,4 +244,6 @@ export default {
'caseManagement.featureCase.fileIsUpdated': 'File is updated',
'caseManagement.featureCase.selectTransferDirectory': 'Please select the transfer directory',
'caseManagement.featureCase.quicklyCreateDefectSuccess': 'Quick bug creation success',
'caseManagement.featureCase.cancelDependencyTip': 'Confirm cancel dependencies?',
'caseManagement.featureCase.cancelDependencyContent': 'Cancel after impact test plan related statistics',
};

View File

@ -239,4 +239,6 @@ export default {
'caseManagement.featureCase.fileIsUpdated': '当前文件已更新',
'caseManagement.featureCase.selectTransferDirectory': '请选择转存目录',
'caseManagement.featureCase.quicklyCreateDefectSuccess': '快速创建缺陷成功',
'caseManagement.featureCase.cancelDependencyTip': '确认取消依赖关系吗?',
'caseManagement.featureCase.cancelDependencyContent': '取消后,影响测试计划相关统计',
};

View File

@ -0,0 +1,160 @@
<template>
<MsDrawer
v-model:visible="showScriptDrawer"
:title="t('ms.case.associate.title')"
:width="768"
:footer="false"
unmount-on-close
>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item
field="name"
:label="t('project.commonScript.publicScriptName')"
:rules="[{ required: true, message: t('project.commonScript.publicScriptNameNotEmpty') }]"
>
<a-input
v-model="form.name"
:max-length="255"
show-word-limit
:placeholder="t('project.commonScript.pleaseEnterScriptName')"
/>
</a-form-item>
<a-form-item field="enable" :label="t('project.commonScript.scriptEnabled')">
<a-select class="max-w-[396px]" :placeholder="t('project.commonScript.scriptEnabled')">
<a-option>{{ t('project.commonScript.draft') }}</a-option>
<a-option>{{ t('project.commonScript.testsPass') }}</a-option>
</a-select>
</a-form-item>
<a-form-item field="description" :label="t('system.organization.description')">
<a-textarea
v-model="form.description"
:placeholder="t('system.organization.descriptionPlaceholder')"
allow-clear
:auto-size="{ minRows: 1 }"
/>
</a-form-item>
<a-form-item field="tags" :label="t('system.organization.description')">
<a-input-tag :placeholder="t('project.commonScript.enterContentAddTags')" allow-clear />
</a-form-item>
<a-form-item field="inputParameters" :label="t('project.commonScript.inputParams')">
<ms-base-table v-bind="propsRes" ref="tableRef" no-disable v-on="propsEvent"> </ms-base-table>
</a-form-item>
<div class="mb-2 flex items-center justify-between">
<a-radio-group v-model:model-value="scriptType" type="button" size="small">
<a-radio value="commonScript">{{ t('project.commonScript.commonScript') }}</a-radio>
<a-radio value="executionResult">{{ t('project.commonScript.executionResult') }}</a-radio>
</a-radio-group>
<a-button type="outline">{{ t('project.commonScript.scriptTest') }}</a-button>
</div>
<ScriptDefined v-if="scriptType === 'commonScript'" />
<div v-else>
<MsCodeEditor
v-model:model-value="executionResultValue"
title=""
width="100%"
height="calc(100vh - 155px)"
theme="MS-text"
:read-only="false"
:show-full-screen="false"
:show-theme-change="false"
/>
</div>
</a-form>
</MsDrawer>
</template>
<script setup lang="ts">
import { computed, 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 ScriptDefined from './scriptDefined.vue';
import { useI18n } from '@/hooks/useI18n';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits(['update:visible']);
const showScriptDrawer = computed({
get() {
return props.visible;
},
set(val) {
emit('update:visible', val);
},
});
const initForm = {
name: '',
description: '',
};
const form = ref({ ...initForm });
const columns: MsTableColumn = [
{
title: '参数名称',
slotName: 'name',
dataIndex: 'name',
showTooltip: true,
showInTable: true,
},
{
title: '是否必填',
slotName: 'required',
dataIndex: 'required',
showInTable: true,
},
{
title: '参数值',
dataIndex: 'tags',
slotName: 'tags',
showTooltip: true,
showInTable: true,
},
{
title: '描述',
slotName: 'desc',
dataIndex: 'desc',
showTooltip: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
() =>
Promise.resolve({
list: [],
current: 1,
pageSize: 10,
total: 2,
}),
{
columns,
tableKey: TableKeyEnum.FILE_MANAGEMENT_FILE,
showSetting: false,
scroll: { x: '100%' },
heightUsed: 300,
},
(item) => {
return {
...item,
tags: item.tags?.map((e: string) => ({ id: e, name: e })) || [],
};
}
);
const scriptType = ref<'commonScript' | 'executionResult'>('commonScript');
const executionResultValue = ref('');
</script>
<style scoped></style>

View File

@ -0,0 +1,111 @@
<template>
<div 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">
<template #icon><icon-undo class="mr-1 text-[16px] text-[var(--color-text-4)]" /> </template>
{{ t('project.commonScript.undo') }}</MsTag
>
<MsTag theme="outline">
<template #icon>
<icon-eraser class="mr-1 text-[16px] text-[var(--color-text-4)]" />
</template>
{{ t('project.commonScript.clear') }}</MsTag
>
</div>
<MsTag theme="outline">{{ t('project.commonScript.formatting') }}</MsTag>
</div>
</div>
<div class="flex h-[calc(100vh-120px)] bg-[var(--color-bg-3)]">
<div class="leftCodeEditor w-[70%]">
<MsCodeEditor
v-model:model-value="commonScriptValue"
title=""
width="100%"
height="calc(100vh - 155px)"
theme="MS-text"
:read-only="false"
:show-full-screen="false"
:show-theme-change="false"
/>
</div>
<div class="rightCodeEditor mt-[24px] h-[calc(100vh-155px)] 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">
<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 text-[12px]" @click="handleClick(item)">
{{ item.title }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n';
import type { CommonScriptMenu } from '@/models/projectManagement/commonScript';
import { SCRIPT_MENU } from '../utils';
const { t } = useI18n();
const expanded = ref<boolean>(true);
const language = ref('beanshell');
const commonScriptValue = ref('');
const languages = [
{ text: 'beanshell', value: 'beanshell' },
{ text: 'python', value: 'python' },
{ text: 'groovy', value: 'groovy' },
{ text: 'javascript', value: 'javascript' },
];
function expandedHandler() {
expanded.value = !expanded.value;
}
function handleClick(menu: CommonScriptMenu) {}
</script>
<style scoped lang="less">
.collapsebtn {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--color-text-n8) !important;
@apply cursor-pointer bg-white;
}
.expand {
width: 16px;
height: 16px;
border-radius: 50%;
background: rgb(var(--primary-1));
@apply cursor-pointer;
}
.menuItem {
height: 24px;
line-height: 24px;
color: rgb(var(--primary-5));
@apply cursor-pointer;
}
</style>

View File

@ -0,0 +1,226 @@
<template>
<MsCard simple>
<div class="mb-4 flex items-center justify-between">
<a-button type="outline" @click="addCommonScript"> {{ t('project.commonScript.addPublicScript') }} </a-button>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('project.commonScript.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
/>
</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] 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>
<template #operation="{ record }">
<MsButton status="primary">
{{ t('common.edit') }}
</MsButton>
<MsTableMoreAction
v-if="!record.internal"
:list="actions"
@select="(item:ActionsItem) => handleMoreActionSelect(item,record)"
/></template>
</ms-base-table>
<template v-if="(keyword || '').trim() === ''" #empty>
<div class="flex items-center justify-center">
{{ t('caseManagement.caseReview.tableNoData') }}
<MsButton class="ml-[8px]"> {{ t('project.commonScript.addPublicScript') }} </MsButton>
</div>
</template>
<AddScriptDrawer v-model:visible="showScriptDrawer" />
</MsCard>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/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 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 { getDependOnCase } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore, useTableStore } from '@/store';
import { characterLimit } from '@/utils';
import { TableKeyEnum } from '@/enums/tableEnum';
const tableStore = useTableStore();
const { openModal } = useModal();
const { t } = useI18n();
const keyword = ref<string>('');
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,
},
{
title: 'project.commonScript.tableColumnActions',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 140,
showInTable: true,
showDrag: false,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setProps } = useTable(getDependOnCase, {
scroll: { x: '100%' },
tableKey: TableKeyEnum.ORGANIZATION_PROJECT_COMMON_SCRIPT,
selectable: true,
heightUsed: 340,
showSetting: true,
enableDrag: true,
});
const actions: ActionsItem[] = [
{
label: t('common.delete'),
danger: true,
eventTag: 'delete',
},
];
function deleteScript(record: Record<string, any>) {
openModal({
type: 'error',
title: t('project.commonScript.deleteTitleTip', { name: characterLimit(record.name) }),
content: t('project.commonScript.deleteTitleContent'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
Message.success(t('common.deleteSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
hideCancel: false,
});
}
function handleMoreActionSelect(item: ActionsItem, record: Record<string, any>) {
if (item.eventTag === 'delete') {
deleteScript(record);
}
}
onMounted(() => {
// setLoadListParams({});
// loadList();
setProps({
data: [
// {
// name: '',
// description: 'description',
// enable: true,
// tags: [{ id: '1001', name: 'A' }],
// },
],
});
});
tableStore.initColumn(TableKeyEnum.ORGANIZATION_PROJECT_COMMON_SCRIPT, columns, 'drawer');
const showScriptDrawer = ref<boolean>(false);
function addCommonScript() {
showScriptDrawer.value = true;
}
</script>
<style scoped></style>

View File

@ -0,0 +1,46 @@
export default {
'project.commonScript.searchByNameAndId': 'Search by name',
'project.commonScript.addPublicScript': 'Add public Script',
'project.commonScript.name': 'Name',
'project.commonScript.description': 'Description',
'project.commonScript.enable': 'Enable',
'project.commonScript.createUser': 'CreateUser',
'project.commonScript.createTime': 'CreateTime',
'project.commonScript.updateTime': 'UpdateTime',
'project.commonScript.tableColumnActions': 'Operation',
'project.commonScript.tags': 'tags',
'project.commonScript.deleteTitleTip': 'Confirm delete {name} script?',
'project.commonScript.deleteTitleContent':
'After deletion, the script may cause test cases that refer to it to fail, so be careful!',
'project.commonScript.preview': 'Preview',
'project.commonScript.testsPass': 'Pass the test',
'project.commonScript.draft': 'draft',
'project.commonScript.publicScriptName': 'Common script name',
'project.commonScript.publicScriptNameNotEmpty': 'The public script name cannot be empty',
'project.commonScript.pleaseEnterScriptName': 'Please enter a script name',
'project.commonScript.scriptEnabled': 'Script state',
'project.commonScript.enterContentAddTags': 'Enter the content to add the label directly',
'project.commonScript.inputParams': 'In parameter',
'project.commonScript.commonScript': 'Common script',
'project.commonScript.executionResult': 'Execution result',
'project.commonScript.scriptTest': 'test',
'project.commonScript.codeSnippet': 'Code snippet',
'project.commonScript.pleaseSelected': 'Please select',
'project.commonScript.formatting': 'formatting',
'project.commonScript.undo': 'cancel',
'project.commonScript.clear': 'clear',
'code_segment': {
importApiTest: 'Import from API definition',
newApiTest: 'New API test[JSON]',
},
'processor': {
codeTemplateGetVariable: 'Get Variable',
codeTemplateSetVariable: 'Set Variable',
codeTemplateGetResponseHeader: 'Get Response Header',
codeTemplateGetResponseCode: 'Get Response Code',
codeTemplateGetResponseResult: 'Get Response Result',
paramEnvironmentSetGlobalVariable: 'Set run environment param',
insertPublicScript: 'Insert the public script',
terminationTest: 'Termination test',
},
};

View File

@ -0,0 +1,47 @@
export default {
'project.commonScript.searchByNameAndId': '通过名称搜索',
'project.commonScript.addPublicScript': '添加公共脚本',
'project.commonScript.name': '名称',
'project.commonScript.description': '描述',
'project.commonScript.enable': '状态',
'project.commonScript.createUser': '创建人',
'project.commonScript.createTime': '创建时间',
'project.commonScript.updateTime': '更新时间',
'project.commonScript.tableColumnActions': '操作',
'project.commonScript.tags': '标签',
'project.commonScript.deleteTitleTip': '确认删除 {name} 脚本吗?',
'project.commonScript.deleteTitleContent': '删除后,脚本可能会导致引用该脚本的测试用例执行失败,请谨慎操作!',
'project.commonScript.preview': '预览',
'project.commonScript.testsPass': '测试通过',
'project.commonScript.draft': '草稿',
'project.commonScript.publicScriptName': '公共脚本名称',
'project.commonScript.publicScriptNameNotEmpty': '公共脚本名称',
'project.commonScript.pleaseEnterScriptName': '请输入脚本名称',
'project.commonScript.scriptEnabled': '脚本状态',
'project.commonScript.enterContentAddTags': '输入内容后回车可直接添加标签',
'project.commonScript.inputParams': '入参',
'project.commonScript.commonScript': '公共脚本',
'project.commonScript.executionResult': '执行结果',
'project.commonScript.scriptTest': '测试',
'project.commonScript.codeSnippet': '代码片段',
'project.commonScript.pleaseSelected': '请选择',
'project.commonScript.formatting': '格式化',
'project.commonScript.undo': '撤销',
'project.commonScript.clear': '清空',
'project': {
code_segment: {
importApiTest: '从API定义导入',
newApiTest: '新API测试(JSON)',
},
processor: {
codeTemplateGetVariable: '获取变量',
codeTemplateSetVariable: '设置变量',
codeTemplateGetResponseHeader: '获取响应头',
codeTemplateGetResponseCode: '获取响应码',
codeTemplateGetResponseResult: '获取响应结果',
paramEnvironmentSetGlobalVariable: '设置环境参数',
insertPublicScript: '插入公共脚本',
terminationTest: '终止测试',
},
},
};

View File

@ -0,0 +1,55 @@
import { useI18n } from '@/hooks/useI18n';
import type { CommonScriptMenu } from '@/models/projectManagement/commonScript';
const { t } = useI18n();
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: 'terminal_function',
command: 'terminal_function',
},
];
export default {};

View File

@ -101,7 +101,7 @@
asterisk-position="end"
class="max-w-[732px]"
>
<MsRichText v-model:model-value="defectForm.description" />
<MsRichText v-model:raw="defectForm.description" />
<MsFormItemSub :text="t('system.orgTemplate.defectContentTip')" :show-fill-icon="false" />
</a-form-item>
</a-form>

View File

@ -17,7 +17,7 @@
></a-input>
</a-form-item>
<a-form-item field="precondition" :label="t('system.orgTemplate.precondition')" asterisk-position="end">
<MsRichText v-model="viewForm.precondition" />
<MsRichText v-model:raw="viewForm.precondition" />
</a-form-item>
<a-form-item field="step" :label="t('system.orgTemplate.stepDescription')" class="relative">
<div class="absolute left-16 top-0">

View File

@ -17,7 +17,7 @@
></a-input>
</a-form-item>
<a-form-item field="precondition" :label="t('system.orgTemplate.defectContent')" asterisk-position="end">
<MsRichText v-model="viewForm.description" />
<MsRichText v-model:raw="viewForm.description" />
</a-form-item>
<a-form-item field="attachment" label="添加附件">
<div class="flex flex-col">

View File

@ -101,7 +101,7 @@
asterisk-position="end"
class="max-w-[732px]"
>
<MsRichText v-model:model-value="defectForm.description" />
<MsRichText v-model:raw="defectForm.description" />
<MsFormItemSub :text="t('system.orgTemplate.defectContentTip')" :show-fill-icon="false" />
</a-form-item>
</a-form>

View File

@ -281,7 +281,7 @@
}
</script>
<style scoped lang="scss">
<style scoped lang="less">
.wrapper {
width: 100%;
min-width: 112px;

View File

@ -77,7 +77,6 @@
<a-tooltip>
<template #content>
<div class="text-sm">{{ t('system.plugin.statusEnableTip') }}</div>
<div class="text-sm">{{ t('system.plugin.statusDisableTip') }}</div>
</template>
<div class="mx-1 flex h-[32px] items-center">
<span class="mr-1">{{ t('system.plugin.pluginStatus') }}</span>