feat(系统设置): 项目创建编辑

This commit is contained in:
RubyLiu 2023-08-22 14:46:10 +08:00 committed by fit2-zhao
parent 0221a76d45
commit f1f99af196
11 changed files with 209 additions and 85 deletions

View File

@ -5,6 +5,7 @@ import { AddUserToOrgOrProjectParams } from '@/models/setting/systemOrg';
import {
CreateOrUpdateSystemOrgParams,
CreateOrUpdateSystemProjectParams,
SystemGetUserByOrgOrProjectIdParams,
} from '@/models/setting/system/orgAndProject';
// 获取组织列表
@ -52,9 +53,9 @@ export function postProjectTableByOrgId(data: TableQueryParams) {
return MSR.post({ url: orgUrl.postProjectTableByOrgUrl, data });
}
// 根据组织id获取用户列表
export function postUserTableByOrgId(data: TableQueryParams) {
return MSR.post({ url: orgUrl.postOrgMemberUrl, data });
// 根据 orgId 或 projectId 获取用户列表
export function postUserTableByOrgIdOrProjectId(data: SystemGetUserByOrgOrProjectIdParams) {
return MSR.post({ url: data.organizationId ? orgUrl.postOrgMemberUrl : orgUrl.postProjectMemberUrl, data });
}
// 给组织或项目添加成员
export function addUserToOrgOrProject(data: AddUserToOrgOrProjectParams) {
@ -71,7 +72,22 @@ export function deleteUserFromOrgOrProject(sourceId: string, userId: string, isO
});
}
// TODO: 等待后端同学的接口 启用或禁用项目
// 启用或禁用项目
export function enableOrDisableProject(id: string, isEnable = true) {
return MSR.get({ url: `${isEnable ? orgUrl.getEnableProjectUrl : orgUrl.getDisableProjectUrl}${id}` });
}
// 获取组织下拉选项
export function getSystemOrgOption() {
return MSR.post({ url: orgUrl.postOrgOptionsUrl });
}
// 创建或更新项目
export function createOrUpdateProject(data: CreateOrUpdateSystemProjectParams) {
return MSR.post({ url: data.id ? orgUrl.postModifyProjectUrl : orgUrl.postAddProjectUrl, data });
}
// 创建项目或组织时获取所有用户
export function getAllUser() {
return MSR.get({ url: orgUrl.getOrgOrProjectAdminUrl });
}

View File

@ -50,3 +50,5 @@ export const getUserByOrgOrProjectUrl = '/system/user/get-option/';
export const getEnableProjectUrl = '/system/project/enable/';
// 禁用项目
export const getDisableProjectUrl = '/system/project/disable/';
// 获取组织或项目的管理员
export const getOrgOrProjectAdminUrl = '/system/project/user-list';

View File

@ -23,8 +23,7 @@
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import { ref, onMounted, watch } from 'vue';
import { getUserList } from '@/api/modules/setting/usergroup';
import { getUserByOrganizationOrProject } from '@/api/modules/setting/system/organizationAndProject';
import { getUserByOrganizationOrProject, getAllUser } from '@/api/modules/setting/system/organizationAndProject';
export interface MsUserSelectorProps {
value: string[];
@ -66,7 +65,7 @@
}
res = await getUserByOrganizationOrProject(props.sourceId);
} else {
res = await getUserList();
res = await getAllUser();
}
res.forEach((item) => {
item.disabled = item[props.disabledKey];

View File

@ -1,3 +1,5 @@
import { TableQueryParams } from '@/models/common';
export interface CreateOrUpdateSystemOrgParams {
id?: string;
name: string;
@ -6,13 +8,26 @@ export interface CreateOrUpdateSystemOrgParams {
}
export interface CreateOrUpdateSystemProjectParams {
id?: string;
// 项目名称
name: string;
// 项目描述
description: string;
// 启用或禁用
enable: boolean;
// 项目成员
userIds: string[];
// 模块配置 后端需要的字段 JSON string
moduleSetting?: string;
// 前端展示的模块配置 string[]
module?: string[];
// 模块配置
moduleIds?: string[];
// 所属组织
organizationId?: string;
// 列表里的
}
export interface SystemOrgOption {
id: string;
name: string;
}
export interface SystemGetUserByOrgOrProjectIdParams extends TableQueryParams {
projectId?: string;
organizationId?: string;
}

View File

@ -13,7 +13,7 @@
<span class="text-[var(--color-text-4)]">({{ props.currentProject?.name }})</span>
</span>
<span v-else>
{{ t('system.project.create') }}
{{ t('system.project.createProject') }}
</span>
</template>
<div class="form">
@ -26,8 +26,21 @@
>
<a-input v-model="form.name" :placeholder="t('system.project.projectNamePlaceholder')" />
</a-form-item>
<a-form-item field="organizationId" :label="t('system.project.affiliatedOrg')">
<a-input v-model="form.organizationId" :placeholder="t('system.project.affiliatedOrgPlaceholder')" />
<a-form-item
required
field="organizationId"
:label="t('system.project.affiliatedOrg')"
:rules="[{ required: true, message: t('system.project.affiliatedOrgRequired') }]"
>
<a-select
v-model="form.organizationId"
:disabled="!isXpack"
allow-search
:options="affiliatedOrgOption"
:placeholder="t('system.project.affiliatedOrgPlaceholder')"
:field-names="{ label: 'name', value: 'id' }"
>
</a-select>
</a-form-item>
<a-form-item field="userIds" :label="t('system.project.projectAdmin')">
<MsUserSelector v-model:value="form.userIds" placeholder="system.project.projectAdminPlaceholder" />
@ -35,18 +48,33 @@
<a-form-item field="description" :label="t('system.organization.description')">
<a-input v-model="form.description" :placeholder="t('system.organization.descriptionPlaceholder')" />
</a-form-item>
<a-form-item field="enable" :label="t('system.organization.description')">
<a-switch v-model="form.enable" :placeholder="t('system.organization.descriptionPlaceholder')" />
<a-form-item field="module" :label="t('system.organization.description')">
<a-checkbox-group v-model="form.moduleIds" :options="moduleOption">
<template #label="{ data }">
<span>{{ t(data.label) }}</span>
</template>
</a-checkbox-group>
</a-form-item>
</a-form>
</div>
<template #footer>
<a-button type="secondary" :loading="loading" @click="handleCancel">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" :loading="loading" @click="handleBeforeOk">
{{ isEdit ? t('common.confirm') : t('common.create') }}
</a-button>
<div class="flex flex-row justify-between">
<div class="flex flex-row items-center gap-[4px]">
<a-switch v-model="form.enable" />
<span>{{ t('system.organization.status') }}</span>
<a-tooltip :content="t('system.project.createTip')" position="top">
<MsIcon type="icon-icon-maybe_outlined" class="text-[var(--color-text-4)]" />
</a-tooltip>
</div>
<div class="flex flex-row gap-[14px]">
<a-button type="secondary" :loading="loading" @click="handleCancel">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" :loading="loading" @click="handleBeforeOk">
{{ isEdit ? t('common.confirm') : t('common.create') }}
</a-button>
</div>
</div>
</template>
</a-modal>
</template>
@ -56,9 +84,11 @@
import { reactive, ref, watchEffect, computed } from 'vue';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
import MsUserSelector from '@/components/business/ms-user-selector/index.vue';
import { createOrUpdateOrg } from '@/api/modules/setting/system/organizationAndProject';
import { createOrUpdateProject, getSystemOrgOption } from '@/api/modules/setting/system/organizationAndProject';
import { Message } from '@arco-design/web-vue';
import { CreateOrUpdateSystemProjectParams } from '@/models/setting/system/orgAndProject';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { CreateOrUpdateSystemProjectParams, SystemOrgOption } from '@/models/setting/system/orgAndProject';
import useLicenseStore from '@/store/modules/setting/license';
const { t } = useI18n();
const props = defineProps<{
@ -70,6 +100,17 @@
const loading = ref(false);
const isEdit = computed(() => !!props.currentProject?.id);
const affiliatedOrgOption = ref<SystemOrgOption[]>([]);
const licenseStore = useLicenseStore();
const moduleOption = [
{ label: 'menu.workplace', value: 'workstation' },
{ label: 'menu.testPlan', value: 'testPlan' },
{ label: 'menu.bugManagement', value: 'bugManagement' },
{ label: 'menu.featureTest', value: 'caseManagement' },
{ label: 'menu.apiTest', value: 'apiTest' },
{ label: 'menu.uiTest', value: 'uiTest' },
{ label: 'menu.performanceTest', value: 'loadTest' },
];
const emit = defineEmits<{
(e: 'cancel'): void;
@ -81,49 +122,64 @@
organizationId: '',
description: '',
enable: true,
module: [],
moduleIds: [],
});
const currentVisible = ref(props.visible);
const isXpack = computed(() => {
return licenseStore.hasLicense();
});
watchEffect(() => {
currentVisible.value = props.visible;
});
const handleCancel = () => {
formRef.value?.resetFields();
emit('cancel');
};
const handleBeforeOk = () => {
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
const handleBeforeOk = async () => {
await formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (errors) {
return false;
return;
}
try {
loading.value = true;
await createOrUpdateOrg({ id: props.currentProject?.id, ...form });
await createOrUpdateProject({ id: props.currentProject?.id, ...form });
Message.success(
isEdit.value
? t('system.organization.updateOrganizationSuccess')
: t('system.organization.createOrganizationSuccess')
);
handleCancel();
return true;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
return false;
} finally {
loading.value = false;
}
});
};
const initAffiliatedOrgOption = async () => {
try {
const res = await getSystemOrgOption();
affiliatedOrgOption.value = res;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
};
watchEffect(() => {
initAffiliatedOrgOption();
if (props.currentProject) {
form.id = props.currentProject.id;
form.name = props.currentProject.name;
form.userIds = props.currentProject.userIds;
form.description = props.currentProject.description;
form.organizationId = props.currentProject.organizationId;
form.enable = props.currentProject.enable;
form.userIds = props.currentProject.userIds;
form.organizationId = props.currentProject.organizationId;
form.moduleIds = props.currentProject.moduleIds;
}
});
</script>

View File

@ -54,7 +54,7 @@
import useTable from '@/components/pure/ms-table/useTable';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { useTableStore } from '@/store';
import { ref, reactive, watch } from 'vue';
import { ref, reactive } from 'vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import {
postOrgTable,
@ -140,7 +140,6 @@
scroll: { y: 'auto', x: '1300px' },
selectable: false,
noDisable: false,
debug: true,
size: 'default',
showSetting: true,
});
@ -208,6 +207,7 @@
Message.success(isEnable ? t('common.enableSuccess') : t('common.closeSuccess'));
fetchData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
@ -274,14 +274,6 @@
setLoading(false);
}
};
watch(
() => props.keyword,
() => {
fetchData();
},
{ immediate: true }
);
defineExpose({
fetchData,
});

View File

@ -28,7 +28,7 @@
<MsButton @click="handleDelete(record)">{{ t('common.delete') }}</MsButton>
</template>
<template v-else>
<MsButton @click="showOrganizationModal(record)">{{ t('common.edit') }}</MsButton>
<MsButton @click="showAddProjectModal(record)">{{ t('common.edit') }}</MsButton>
<MsButton @click="showAddUserModal(record)">{{ t('system.organization.addMember') }}</MsButton>
<MsButton @click="handleEnableOrDisableProject(record, false)">{{ t('common.end') }}</MsButton>
<MsTableMoreAction :list="tableActions" @select="handleMoreAction($event, record)"></MsTableMoreAction>
@ -36,18 +36,12 @@
</template>
</MsBaseTable>
<AddProjectModal
type="edit"
:current-organization="currentUpdateOrganization"
:visible="orgVisible"
@cancel="handleAddOrgModalCancel"
:current-project="currentUpdateProject"
:visible="addProjectVisible"
@cancel="handleAddProjectModalCancel"
/>
<AddUserModal :project-id="currentProjectId" :visible="userVisible" @cancel="handleAddUserModalCancel" />
<UserDrawer
:project-id="currentProjectId"
type="project"
v-bind="currentUserDrawer"
@cancel="handleUserDrawerCancel"
/>
<UserDrawer :project-id="currentProjectId" v-bind="currentUserDrawer" @cancel="handleUserDrawerCancel" />
</template>
<script lang="ts" setup>
@ -55,7 +49,7 @@
import useTable from '@/components/pure/ms-table/useTable';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { useTableStore } from '@/store';
import { ref, reactive, watch } from 'vue';
import { ref, reactive } from 'vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import {
postProjectTable,
@ -71,8 +65,9 @@
import UserDrawer from './userDrawer.vue';
import AddUserModal from './addUserModal.vue';
import useModal from '@/hooks/useModal';
import { CreateOrUpdateSystemOrgParams } from '@/models/setting/system/orgAndProject';
import { CreateOrUpdateSystemProjectParams } from '@/models/setting/system/orgAndProject';
import AddProjectModal from './addProjectModal.vue';
import { UserItem } from '@/components/bussiness/ms-user-selector/index.vue';
export interface SystemOrganizationProps {
keyword: string;
@ -83,9 +78,9 @@
const { t } = useI18n();
const tableStore = useTableStore();
const userVisible = ref(false);
const orgVisible = ref(false);
const addProjectVisible = ref(false);
const currentProjectId = ref('');
const currentUpdateOrganization = ref<CreateOrUpdateSystemOrgParams>();
const currentUpdateProject = ref<CreateOrUpdateSystemProjectParams>();
const { openDeleteModal, openModal } = useModal();
const organizationColumns: MsTableColumn = [
@ -145,7 +140,6 @@
scroll: { y: 'auto', x: '1300px' },
selectable: false,
noDisable: false,
debug: true,
size: 'default',
showSetting: true,
});
@ -215,14 +209,17 @@
});
};
const showOrganizationModal = (record: any) => {
currentProjectId.value = record.id;
orgVisible.value = true;
currentUpdateOrganization.value = {
id: record.id,
name: record.name,
description: record.description,
memberIds: record.orgAdmins.map((item: any) => item.id) || [],
const showAddProjectModal = (record: any) => {
const { id, name, description, enable, adminList, organizationId, moduleIds } = record;
addProjectVisible.value = true;
currentUpdateProject.value = {
id,
name,
description,
enable,
userIds: adminList.map((item: UserItem) => item.id),
organizationId,
moduleIds,
};
};
@ -245,8 +242,8 @@
userVisible.value = false;
fetchData();
};
const handleAddOrgModalCancel = () => {
orgVisible.value = false;
const handleAddProjectModalCancel = () => {
addProjectVisible.value = false;
fetchData();
};
@ -270,14 +267,6 @@
hideCancel: false,
});
};
watch(
() => props.keyword,
() => {
fetchData();
},
{ immediate: true }
);
defineExpose({
fetchData,
});

View File

@ -48,7 +48,7 @@
<script lang="ts" setup>
import {
postUserTableByOrgId,
postUserTableByOrgIdOrProjectId,
deleteUserFromOrgOrProject,
} from '@/api/modules/setting/system/organizationAndProject';
import { MsTableColumn } from '@/components/pure/ms-table/type';
@ -95,7 +95,7 @@
{ title: 'system.organization.operation', slotName: 'operation' },
];
const { propsRes, propsEvent, loadList, setLoadListParams, setKeyword } = useTable(postUserTableByOrgId, {
const { propsRes, propsEvent, loadList, setLoadListParams, setKeyword } = useTable(postUserTableByOrgIdOrProjectId, {
columns: projectColumn,
showSetting: false,
scroll: { y: 'auto', x: '600px' },
@ -148,6 +148,12 @@
fetchData();
}
);
watch(
() => props.projectId,
() => {
fetchData();
}
);
watch(
() => props.visible,
(visible) => {

View File

@ -33,7 +33,7 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, onMounted, watch, nextTick } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import MsCard from '@/components/pure/ms-card/index.vue';
import AddOrganizationModal from './components/addOrganizationModal.vue';
@ -51,11 +51,31 @@
const projectTabeRef = ref();
const projectVisible = ref(false);
const tableSearch = () => {
if (currentTable.value === 'organization') {
if (orgTableRef.value) {
orgTableRef.value.fetchData();
} else {
nextTick(() => {
orgTableRef.value?.fetchData();
});
}
} else if (projectTabeRef.value) {
projectTabeRef.value.fetchData();
} else {
nextTick(() => {
projectTabeRef.value?.fetchData();
});
}
};
const handleSearch = (value: string) => {
currentKeyword.value = value;
tableSearch();
};
const handleEnter = (eve: Event) => {
currentKeyword.value = (eve.target as HTMLInputElement).value;
tableSearch();
};
const handleAddOrganization = () => {
@ -67,14 +87,20 @@
};
const handleAddProjectCancel = () => {
tableSearch();
projectVisible.value = false;
};
const handleAddOrganizationCancel = () => {
if (currentTable.value === 'organization') {
orgTableRef.value?.fetchData();
} else {
projectTabeRef.value?.fetchData();
}
tableSearch();
organizationVisible.value = false;
};
watch(
() => currentTable.value,
() => {
tableSearch();
}
);
onMounted(() => {
tableSearch();
});
</script>

View File

@ -59,4 +59,16 @@ export default {
'system.project.endTitle': 'Close project',
'system.project.enableContent': 'The project after opening is displayed in the organization switching list',
'system.project.endContent': 'The project after closing is not displayed in the project switching list',
'system.project.projectNamePlaceholder':
'Please enter the project name, which cannot be duplicated with other project names',
'system.project.updateProject': 'Update project',
'system.project.createProject': 'Create project',
'system.project.affiliatedOrg': 'Affiliated organization',
'system.project.affiliatedOrgPlaceholder': 'Please select affiliated organization',
'system.project.projectAdmin': 'Project administrator',
'system.project.projectAdminPlaceholder': 'The project administrator defaults to the person who created the project',
'system.project.moduleSetting': 'Module setting',
'system.project.projectNameRequired': 'Project name cannot be empty',
'system.project.createTip': 'After the project is enabled, it will be displayed in the project switching list',
'system.project.affiliatedOrgRequired': 'Affiliated organization cannot be empty',
};

View File

@ -55,4 +55,15 @@ export default {
'system.project.endTitle': '关闭项目',
'system.project.enableContent': '开启后的项目展示在项目切换列表',
'system.project.endContent': '关闭后的项目不展示在项目切换列表',
'system.project.projectNamePlaceholder': '请输入项目名称,不可与其他项目名称重复',
'system.project.updateProject': '更新项目',
'system.project.createProject': '创建项目',
'system.project.affiliatedOrg': '所属组织',
'system.project.affiliatedOrgPlaceholder': '请选择所属组织',
'system.project.projectAdmin': '项目管理员',
'system.project.projectAdminPlaceholder': '默认选择创建项目人为项目管理员',
'system.project.moduleSetting': '模块设置',
'system.project.projectNameRequired': '项目名称不能为空',
'system.project.createTip': '项目启用后,将展示在项目切换列表',
'system.project.affiliatedOrgRequired': '所属组织不能为空',
};