fix(router): check system permissions against target project
This commit is contained in:
@@ -47,8 +47,8 @@ describe("project management access", () => {
|
||||
expect(source).toContain("suppressErrorMessage: true");
|
||||
expect(source).toContain("const canProject = (row: Study, module: string, action: \"read\" | \"write\")");
|
||||
expect(source).toContain("canProject(scope.row, 'project_members', 'read')");
|
||||
expect(source).toContain("canProject(scope.row, 'project_members', 'write')");
|
||||
expect(source).toContain("canProject(scope.row, 'sites', 'read')");
|
||||
expect(source).toContain("canConfigureProject(scope.row)");
|
||||
expect(source).toContain("canManageProjectBackend(scope.row)");
|
||||
expect(source).toContain("canProject(scope.row, 'audit_export', 'read')");
|
||||
});
|
||||
|
||||
@@ -74,13 +74,33 @@ describe("project management access", () => {
|
||||
expect(source).not.toContain('fixed="right"');
|
||||
});
|
||||
|
||||
it("opens project names in management detail when the user has setup config read permission", () => {
|
||||
it("renders project names as plain text and moves setup configuration to a gear action", () => {
|
||||
const source = readProjects();
|
||||
|
||||
expect(source).toContain("v-if=\"canProject(scope.row, 'setup_config', 'read')\"");
|
||||
expect(source).toContain('@click="goProject(scope.row)" class="project-name"');
|
||||
expect(source).not.toContain("<el-link");
|
||||
expect(source).not.toContain('@click="goProject(scope.row)" class="project-name"');
|
||||
expect(source).toContain('<span class="project-name">{{ scope.row.name }}</span>');
|
||||
expect(source).toContain('v-if="canProject(scope.row, \'setup_config\', \'write\')" content="项目配置"');
|
||||
expect(source).toContain(':icon="Setting"');
|
||||
expect(source).toContain('@click="goProject(scope.row)"');
|
||||
expect(source).toContain('@click="enterStudy(scope.row)"');
|
||||
expect(source).toContain('if (!canProject(row, "setup_config", "read"))');
|
||||
expect(source).toContain('if (!canProject(row, "setup_config", "write"))');
|
||||
});
|
||||
|
||||
it("shows project permission configuration only for admins and target project PMs", () => {
|
||||
const source = readProjects();
|
||||
|
||||
expect(source).toContain('v-if="canConfigureProject(scope.row)" content="权限管理"');
|
||||
expect(source).toContain("const canConfigureProject = (row: Study) => canManageProjectBackend(row);");
|
||||
expect(source).not.toContain("canProject(scope.row, 'project_members', 'write')");
|
||||
});
|
||||
|
||||
it("shows center management only for admins and target project PMs", () => {
|
||||
const source = readProjects();
|
||||
|
||||
expect(source).toContain('v-if="canManageProjectBackend(scope.row)" :content="TEXT.modules.adminProjects.sites"');
|
||||
expect(source).toContain("const canManageProjectBackend = (row: Study) => isAdmin.value || row.role_in_study === \"PM\";");
|
||||
expect(source).not.toContain("canProject(scope.row, 'sites', 'read')");
|
||||
});
|
||||
|
||||
it("checks project management actions against the current row role instead of the first permission role", () => {
|
||||
|
||||
@@ -74,8 +74,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="project-info">
|
||||
<el-link v-if="canProject(scope.row, 'setup_config', 'read')" type="primary" @click="goProject(scope.row)" class="project-name">{{ scope.row.name }}</el-link>
|
||||
<span v-else class="project-name project-name--disabled">{{ scope.row.name }}</span>
|
||||
<span class="project-name">{{ scope.row.name }}</span>
|
||||
<span class="project-code">{{ scope.row.code || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -123,10 +122,13 @@
|
||||
<el-tooltip v-if="canProject(scope.row, 'project_members', 'read')" :content="TEXT.modules.adminProjects.members" placement="top">
|
||||
<el-button link type="primary" :icon="User" class="action-btn" @click="goMembers(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="canProject(scope.row, 'project_members', 'write')" content="权限管理" placement="top">
|
||||
<el-tooltip v-if="canProject(scope.row, 'setup_config', 'write')" content="项目配置" placement="top">
|
||||
<el-button link type="primary" :icon="Setting" class="action-btn" @click="goProject(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="canConfigureProject(scope.row)" content="权限管理" placement="top">
|
||||
<el-button link type="primary" :icon="Key" class="action-btn" @click="goPermissions(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="canProject(scope.row, 'sites', 'read')" :content="TEXT.modules.adminProjects.sites" placement="top">
|
||||
<el-tooltip v-if="canManageProjectBackend(scope.row)" :content="TEXT.modules.adminProjects.sites" placement="top">
|
||||
<el-button link type="primary" :icon="OfficeBuilding" class="action-btn" @click="goSites(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="canProject(scope.row, 'audit_export', 'read')" :content="TEXT.menu.auditLogs" placement="top">
|
||||
@@ -155,7 +157,7 @@
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { User, OfficeBuilding, ArrowRight, Delete, Lock, Key, Document, Plus } from "@element-plus/icons-vue";
|
||||
import { User, OfficeBuilding, ArrowRight, Delete, Lock, Key, Document, Plus, Setting } from "@element-plus/icons-vue";
|
||||
import { fetchStudies, deleteStudy, lockStudy } from "../../api/studies";
|
||||
import { fetchMyApiEndpointPermissions } from "../../api/projectPermissions";
|
||||
import type { ApiEndpointPermissionsResponse, Study } from "../../types/api";
|
||||
@@ -232,15 +234,26 @@ const canProject = (row: Study, module: string, action: "read" | "write") => {
|
||||
return isApiPermissionAllowed(rolePerms[operationKey]);
|
||||
};
|
||||
|
||||
const canManageProjectBackend = (row: Study) => isAdmin.value || row.role_in_study === "PM";
|
||||
const canConfigureProject = (row: Study) => canManageProjectBackend(row);
|
||||
|
||||
const goMembers = (row: Study) => {
|
||||
router.push(`/admin/permissions/project?projectId=${row.id}&sub=members`);
|
||||
};
|
||||
|
||||
const goSites = (row: Study) => {
|
||||
if (!canManageProjectBackend(row)) {
|
||||
ElMessage.warning("仅项目 PM 可管理该项目中心");
|
||||
return;
|
||||
}
|
||||
router.push(`/admin/projects/${row.id}/sites`);
|
||||
};
|
||||
|
||||
const goPermissions = (row: Study) => {
|
||||
if (!canConfigureProject(row)) {
|
||||
ElMessage.warning("仅项目 PM 可配置该项目权限");
|
||||
return;
|
||||
}
|
||||
router.push(`/admin/permissions/project?projectId=${row.id}`);
|
||||
};
|
||||
|
||||
@@ -342,8 +355,8 @@ const goDetail = (row: Study) => {
|
||||
};
|
||||
|
||||
const goProject = (row: Study) => {
|
||||
if (!canProject(row, "setup_config", "read")) {
|
||||
ElMessage.warning("当前角色无权访问该项目的立项配置");
|
||||
if (!canProject(row, "setup_config", "write")) {
|
||||
ElMessage.warning("当前角色无权编辑该项目的立项配置");
|
||||
return;
|
||||
}
|
||||
goDetail(row);
|
||||
@@ -364,7 +377,6 @@ onMounted(() => {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--unified-shell-divider);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@@ -462,13 +474,6 @@ onMounted(() => {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.project-name--disabled {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--ctms-text-secondary);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.project-code {
|
||||
font-size: 12px;
|
||||
color: var(--ctms-text-secondary);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
<div class="unified-section site-table-section section--flush-x section--flush-top section--flush-bottom">
|
||||
<el-table :data="displaySites" v-loading="loading" stripe :row-class-name="siteRowClass" style="width: 100%" class="site-table">
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.siteName" min-width="220">
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.siteName" min-width="140">
|
||||
<template #default="scope">
|
||||
<div class="site-name-cell">
|
||||
<span>{{ scope.row.name }}</span>
|
||||
@@ -19,25 +19,25 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="city" :label="TEXT.common.fields.city" width="100" />
|
||||
<el-table-column prop="pi_name" :label="TEXT.modules.adminSites.piLabel" width="120" />
|
||||
<el-table-column :label="TEXT.common.fields.phone" width="140">
|
||||
<el-table-column prop="city" :label="TEXT.common.fields.city" min-width="80" />
|
||||
<el-table-column prop="pi_name" :label="TEXT.modules.adminSites.piLabel" min-width="100" />
|
||||
<el-table-column :label="TEXT.common.fields.phone" min-width="110">
|
||||
<template #default="scope">
|
||||
{{ phoneText(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="contact" :label="TEXT.common.fields.contact" min-width="160">
|
||||
<el-table-column prop="contact" :label="TEXT.common.fields.contact" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ contactLabel(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.fields.status" width="100">
|
||||
<el-table-column :label="TEXT.common.fields.status" min-width="80">
|
||||
<template #default="scope">
|
||||
<el-tag type="success" v-if="scope.row.is_active">已解锁</el-tag>
|
||||
<el-tag type="danger" v-else>已锁定</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200" align="center" class-name="action-column">
|
||||
<el-table-column :label="TEXT.common.labels.actions" min-width="160" align="center" class-name="action-column">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button
|
||||
|
||||
Reference in New Issue
Block a user