fix(projects): 执行和任务的按钮控制
This commit is contained in:
2
src/typings/api/project.d.ts
vendored
2
src/typings/api/project.d.ts
vendored
@@ -239,7 +239,7 @@ declare namespace Api {
|
|||||||
type: string;
|
type: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
ownerNickname?: string | null;
|
ownerNickname?: string | null;
|
||||||
/** 所属执行的负责人 userId(按钮可见度公式用);跨执行查询永远为 null,按钮判定退化为只看权限码 */
|
/** 所属执行的负责人 userId(按钮可见度公式用);跨执行查询为空时,前端无法识别执行负责人身份 */
|
||||||
executionOwnerId: string | null;
|
executionOwnerId: string | null;
|
||||||
/** 父任务负责人 userId(一级任务为 null) */
|
/** 父任务负责人 userId(一级任务为 null) */
|
||||||
parentTaskOwnerId: string | null;
|
parentTaskOwnerId: string | null;
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
interface TaskMutationIdentity {
|
||||||
|
currentUserId: string;
|
||||||
|
projectManagerUserId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判定对象是否处于可编辑/可操作状态。
|
||||||
|
*
|
||||||
|
* 按按钮可见度矩阵(spec §4.2 / §4.3 注释 `allowEdit === true 即 pending / active 状态`):
|
||||||
|
* 可编辑状态严格 = `pending` OR `active`,**不含 paused / completed / cancelled**。
|
||||||
|
*
|
||||||
|
* 不用 `record.allowEdit === true`:列表 VO 后端不一定下发该字段,
|
||||||
|
* 经 `normalizeProjectExecution` 的 `Boolean(undefined) === false` 会让列表所有行误判为不可编辑。
|
||||||
|
* 直接读 `statusCode` 字段(列表 / 详情 VO 都必下发,状态机核心字段)。
|
||||||
|
*/
|
||||||
|
export function isMutable(record: { statusCode: string }): boolean {
|
||||||
|
return record.statusCode === 'pending' || record.statusCode === 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTaskMutationActor(task: Api.Project.ProjectTask, identity: TaskMutationIdentity): boolean {
|
||||||
|
const { currentUserId, projectManagerUserId } = identity;
|
||||||
|
|
||||||
|
if (!currentUserId) return false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
currentUserId === task.ownerId ||
|
||||||
|
currentUserId === task.executionOwnerId ||
|
||||||
|
(Boolean(projectManagerUserId) && currentUserId === projectManagerUserId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExecutionMutationActor(execution: Api.Project.ProjectExecution, identity: TaskMutationIdentity): boolean {
|
||||||
|
const { currentUserId, projectManagerUserId } = identity;
|
||||||
|
|
||||||
|
if (!currentUserId) return false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
currentUserId === execution.ownerId || (Boolean(projectManagerUserId) && currentUserId === projectManagerUserId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canEditExecutionByIdentity(
|
||||||
|
execution: Api.Project.ProjectExecution,
|
||||||
|
identity: TaskMutationIdentity
|
||||||
|
): boolean {
|
||||||
|
return isMutable(execution) && isExecutionMutationActor(execution, identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canDeleteExecutionByIdentity(
|
||||||
|
execution: Api.Project.ProjectExecution,
|
||||||
|
identity: TaskMutationIdentity
|
||||||
|
): boolean {
|
||||||
|
if (execution.statusCode === 'completed') return false;
|
||||||
|
return isExecutionMutationActor(execution, identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canEditTaskByIdentity(task: Api.Project.ProjectTask, identity: TaskMutationIdentity): boolean {
|
||||||
|
return isMutable(task) && isTaskMutationActor(task, identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canDeleteTaskByIdentity(task: Api.Project.ProjectTask, identity: TaskMutationIdentity): boolean {
|
||||||
|
if (task.statusCode === 'completed') return false;
|
||||||
|
return isTaskMutationActor(task, identity);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { computed, markRaw } from 'vue';
|
import { markRaw } from 'vue';
|
||||||
import { useAuthStore } from '@/store/modules/auth';
|
import { useTaskPermissions } from './use-task-permissions';
|
||||||
import IconMdiClipboardEditOutline from '~icons/mdi/clipboard-edit-outline';
|
import IconMdiClipboardEditOutline from '~icons/mdi/clipboard-edit-outline';
|
||||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||||
@@ -25,19 +25,14 @@ export interface TaskActionEmits {
|
|||||||
/**
|
/**
|
||||||
* 任务行操作按钮的集中装配。
|
* 任务行操作按钮的集中装配。
|
||||||
*
|
*
|
||||||
* 这里只收口任务行按钮的最终显示口径,不改动底层子任务 / 父任务负责人 / 执行负责人那套权限体系:
|
* 这里只收口任务行按钮的最终显示口径;编辑 / 删除身份规则统一落在 useTaskPermissions。
|
||||||
* - 当前用户是任务负责人(task.ownerId): 显示 填报 / 编辑 / 删除
|
* 权限码只做后端兜底,不作为前端按钮展示的单独放行条件。
|
||||||
* - 当前用户是协办人(task.assignees[].userId): 只显示 填报
|
|
||||||
* - 都不是: 也显示 填报
|
|
||||||
*/
|
*/
|
||||||
export function useTaskActions(emits: TaskActionEmits) {
|
export function useTaskActions(emits: TaskActionEmits) {
|
||||||
const authStore = useAuthStore();
|
const { canEditTask, canDeleteTask } = useTaskPermissions();
|
||||||
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
|
||||||
|
|
||||||
function createActions(row: Api.Project.ProjectTask): TaskAction[] {
|
function createActions(row: Api.Project.ProjectTask): TaskAction[] {
|
||||||
const actions: TaskAction[] = [];
|
const actions: TaskAction[] = [];
|
||||||
const isOwner = Boolean(currentUserId.value) && row.ownerId === currentUserId.value;
|
|
||||||
const isCompleted = row.statusCode === 'completed';
|
|
||||||
|
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'report',
|
key: 'report',
|
||||||
@@ -47,11 +42,7 @@ export function useTaskActions(emits: TaskActionEmits) {
|
|||||||
onClick: () => emits.report(row)
|
onClick: () => emits.report(row)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isOwner) {
|
if (canEditTask(row)) {
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCompleted) {
|
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
tooltip: '编辑',
|
tooltip: '编辑',
|
||||||
@@ -59,7 +50,9 @@ export function useTaskActions(emits: TaskActionEmits) {
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
onClick: () => emits.edit(row)
|
onClick: () => emits.edit(row)
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canDeleteTask(row)) {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
tooltip: '删除',
|
tooltip: '删除',
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useAuthStore } from '@/store/modules/auth';
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
import { useObjectContextStore } from '@/store/modules/object-context';
|
import { useObjectContextStore } from '@/store/modules/object-context';
|
||||||
|
import {
|
||||||
|
canDeleteExecutionByIdentity,
|
||||||
|
canDeleteTaskByIdentity,
|
||||||
|
canEditExecutionByIdentity,
|
||||||
|
canEditTaskByIdentity,
|
||||||
|
isMutable
|
||||||
|
} from './task-permission-rules';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务 / 执行按钮可见度集中判定
|
* 任务 / 执行按钮可见度集中判定
|
||||||
*
|
*
|
||||||
* 关键领域规则:
|
* 关键领域规则:
|
||||||
* - 任务负责人本人不能编辑 / 删除自己负责的任务(增删改归上级 / 项目负责人裁决)
|
* - 任务编辑 / 删除入口先按身份收口:任务负责人、任务所属执行负责人、项目负责人
|
||||||
|
* - 权限码只作为后端最终校验的兜底,不单独决定前端按钮可见度
|
||||||
* - 本人能做的:状态推进(含 cancel "退出"任务)、加协办人、在自己任务下新增子任务
|
* - 本人能做的:状态推进(含 cancel "退出"任务)、加协办人、在自己任务下新增子任务
|
||||||
* - 执行负责人对子任务无编辑 / 删除权(子任务归父任务 owner 管)
|
|
||||||
* - 执行负责人能维护一级任务协办人(一级任务 `executionOwnerId` 通道;子任务不开此通道)
|
* - 执行负责人能维护一级任务协办人(一级任务 `executionOwnerId` 通道;子任务不开此通道)
|
||||||
* - 父任务负责人能改 / 删子任务,但不能给子任务加协办人 / 建孙任务 / 推进状态
|
|
||||||
*
|
*
|
||||||
* 权限码来源:`project:*` / `project:execution:*` / `project:task:*` 是**对象域权限码**,
|
* 权限码来源:`project:*` / `project:execution:*` / `project:task:*` 是**对象域权限码**,
|
||||||
* 挂在项目对象上下文里(项目负责人 / 项目协作者等角色),从 objectContextStore.buttonCodes 取,
|
* 挂在项目对象上下文里(项目负责人 / 项目协作者等角色),从 objectContextStore.buttonCodes 取,
|
||||||
@@ -22,37 +28,30 @@ export function useTaskPermissions() {
|
|||||||
|
|
||||||
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
||||||
const buttonCodeSet = computed(() => new Set(objectContextStore.buttonCodes));
|
const buttonCodeSet = computed(() => new Set(objectContextStore.buttonCodes));
|
||||||
|
const currentProjectManagerUserId = computed(() => {
|
||||||
|
const summary = objectContextStore.objectSummary as Api.Project.ProjectContext | null;
|
||||||
|
return summary?.currentProject?.managerUserId || null;
|
||||||
|
});
|
||||||
|
|
||||||
function hasPermission(code: string): boolean {
|
function hasPermission(code: string): boolean {
|
||||||
return buttonCodeSet.value.has(code);
|
return buttonCodeSet.value.has(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判定对象是否处于可编辑/可操作状态。
|
|
||||||
*
|
|
||||||
* 按按钮可见度矩阵(spec §4.2 / §4.3 注释 `allowEdit === true 即 pending / active 状态`):
|
|
||||||
* 可编辑状态严格 = `pending` OR `active`,**不含 paused / completed / cancelled**。
|
|
||||||
*
|
|
||||||
* 不用 `record.allowEdit === true`:列表 VO 后端不一定下发该字段,
|
|
||||||
* 经 `normalizeProjectExecution` 的 `Boolean(undefined) === false` 会让列表所有行误判为不可编辑。
|
|
||||||
* 直接读 `statusCode` 字段(列表 / 详情 VO 都必下发,状态机核心字段)。
|
|
||||||
*/
|
|
||||||
function isMutable(record: { statusCode: string }): boolean {
|
|
||||||
return record.statusCode === 'pending' || record.statusCode === 'active';
|
|
||||||
}
|
|
||||||
|
|
||||||
// —— 执行侧 ——
|
// —— 执行侧 ——
|
||||||
|
|
||||||
function canEditExecution(execution: Api.Project.ProjectExecution): boolean {
|
function canEditExecution(execution: Api.Project.ProjectExecution): boolean {
|
||||||
return (
|
return canEditExecutionByIdentity(execution, {
|
||||||
isMutable(execution) && (hasPermission('project:execution:update') || currentUserId.value === execution.ownerId)
|
currentUserId: currentUserId.value,
|
||||||
);
|
projectManagerUserId: currentProjectManagerUserId.value
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function canDeleteExecution(execution: Api.Project.ProjectExecution): boolean {
|
function canDeleteExecution(execution: Api.Project.ProjectExecution): boolean {
|
||||||
// completed 终态后端硬卡(不允许主动删,级联删除时由后端兜底),前端按钮也拦掉
|
// completed 终态后端硬卡(不允许主动删,级联删除时由后端兜底),前端按钮也拦掉
|
||||||
if (execution.statusCode === 'completed') return false;
|
return canDeleteExecutionByIdentity(execution, {
|
||||||
return hasPermission('project:execution:delete');
|
currentUserId: currentUserId.value,
|
||||||
|
projectManagerUserId: currentProjectManagerUserId.value
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function canChangeExecutionOwner(execution: Api.Project.ProjectExecution): boolean {
|
function canChangeExecutionOwner(execution: Api.Project.ProjectExecution): boolean {
|
||||||
@@ -86,20 +85,17 @@ export function useTaskPermissions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function canEditTask(task: Api.Project.ProjectTask): boolean {
|
function canEditTask(task: Api.Project.ProjectTask): boolean {
|
||||||
if (!isMutable(task)) return false;
|
return canEditTaskByIdentity(task, {
|
||||||
if (hasPermission('project:task:update')) return true;
|
currentUserId: currentUserId.value,
|
||||||
return isTopLevelTask(task)
|
projectManagerUserId: currentProjectManagerUserId.value
|
||||||
? currentUserId.value === task.executionOwnerId
|
});
|
||||||
: currentUserId.value === task.parentTaskOwnerId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function canDeleteTask(task: Api.Project.ProjectTask): boolean {
|
function canDeleteTask(task: Api.Project.ProjectTask): boolean {
|
||||||
// completed 终态后端硬卡(不允许主动删,级联删除时由后端兜底),前端按钮也拦掉
|
return canDeleteTaskByIdentity(task, {
|
||||||
if (task.statusCode === 'completed') return false;
|
currentUserId: currentUserId.value,
|
||||||
if (hasPermission('project:task:delete')) return true;
|
projectManagerUserId: currentProjectManagerUserId.value
|
||||||
return isTopLevelTask(task)
|
});
|
||||||
? currentUserId.value === task.executionOwnerId
|
|
||||||
: currentUserId.value === task.parentTaskOwnerId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function canCreateTopLevelTask(execution: Api.Project.ProjectExecution): boolean {
|
function canCreateTopLevelTask(execution: Api.Project.ProjectExecution): boolean {
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ const { data, loading, getData, getDataByPage, mobilePagination } = useUIPaginat
|
|||||||
} as unknown as TaskPageResponse);
|
} as unknown as TaskPageResponse);
|
||||||
}
|
}
|
||||||
// 统一走跨执行接口:身份维度(my/all)与范围维度(全部/状态/具体执行)自由组合
|
// 统一走跨执行接口:身份维度(my/all)与范围维度(全部/状态/具体执行)自由组合
|
||||||
// 单执行场景下 executionOwnerId 等字段会是 null,按钮权限通过全局权限码 RBAC 控制
|
// 单执行场景下任务详情会带 executionOwnerId;跨执行接口缺失时前端只能识别任务/项目负责人身份
|
||||||
return fetchGetProjectTaskPageCross(props.projectId, buildCrossSearchParams());
|
return fetchGetProjectTaskPageCross(props.projectId, buildCrossSearchParams());
|
||||||
},
|
},
|
||||||
transform: response => transformTaskPage(response, pageNo.value, pageSize.value),
|
transform: response => transformTaskPage(response, pageNo.value, pageSize.value),
|
||||||
|
|||||||
179
tests/task-permission-rules.test.ts
Normal file
179
tests/task-permission-rules.test.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import {
|
||||||
|
canDeleteExecutionByIdentity,
|
||||||
|
canDeleteTaskByIdentity,
|
||||||
|
canEditExecutionByIdentity,
|
||||||
|
canEditTaskByIdentity
|
||||||
|
} from '../src/views/project/project/execution/composables/task-permission-rules';
|
||||||
|
|
||||||
|
function createExecution(overrides: Partial<Api.Project.ProjectExecution> = {}): Api.Project.ProjectExecution {
|
||||||
|
return {
|
||||||
|
id: 'execution-1',
|
||||||
|
projectId: 'project-1',
|
||||||
|
projectRequirementId: null,
|
||||||
|
projectRequirementName: null,
|
||||||
|
projectRequirementStatusCode: null,
|
||||||
|
executionName: 'execution',
|
||||||
|
executionType: null,
|
||||||
|
ownerId: 'execution-owner',
|
||||||
|
ownerNickname: null,
|
||||||
|
statusCode: 'active',
|
||||||
|
statusName: null,
|
||||||
|
terminal: false,
|
||||||
|
allowEdit: true,
|
||||||
|
availableActions: [],
|
||||||
|
plannedStartDate: null,
|
||||||
|
plannedEndDate: null,
|
||||||
|
actualStartDate: null,
|
||||||
|
actualEndDate: null,
|
||||||
|
progressRate: 0,
|
||||||
|
priority: '2',
|
||||||
|
priorityName: null,
|
||||||
|
executionDesc: null,
|
||||||
|
lastStatusReason: null,
|
||||||
|
createTime: '2026-07-07 10:00:00',
|
||||||
|
updateTime: '2026-07-07 10:00:00',
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTask(overrides: Partial<Api.Project.ProjectTask> = {}): Api.Project.ProjectTask {
|
||||||
|
return {
|
||||||
|
id: 'task-1',
|
||||||
|
projectId: 'project-1',
|
||||||
|
executionId: 'execution-1',
|
||||||
|
executionName: '执行',
|
||||||
|
executionStatusCode: 'active',
|
||||||
|
parentTaskId: null,
|
||||||
|
projectRequirementId: null,
|
||||||
|
projectRequirementName: null,
|
||||||
|
projectRequirementStatusCode: null,
|
||||||
|
taskTitle: '任务',
|
||||||
|
type: '',
|
||||||
|
ownerId: 'task-owner',
|
||||||
|
ownerNickname: null,
|
||||||
|
executionOwnerId: 'execution-owner',
|
||||||
|
parentTaskOwnerId: null,
|
||||||
|
statusCode: 'active',
|
||||||
|
statusName: null,
|
||||||
|
terminal: false,
|
||||||
|
allowEdit: true,
|
||||||
|
availableActions: [],
|
||||||
|
progressRate: 0,
|
||||||
|
plannedStartDate: null,
|
||||||
|
plannedEndDate: null,
|
||||||
|
actualStartDate: null,
|
||||||
|
actualEndDate: null,
|
||||||
|
priority: '2',
|
||||||
|
priorityName: null,
|
||||||
|
taskDesc: null,
|
||||||
|
lastStatusReason: null,
|
||||||
|
assignees: null,
|
||||||
|
attachments: [],
|
||||||
|
totalSpentHours: null,
|
||||||
|
createTime: '2026-07-07 10:00:00',
|
||||||
|
updateTime: '2026-07-07 10:00:00',
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('task mutation permission rules', () => {
|
||||||
|
it('allows task owner, execution owner, and project manager to edit mutable tasks', () => {
|
||||||
|
const task = createTask();
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
canEditTaskByIdentity(task, { currentUserId: 'task-owner', projectManagerUserId: 'project-manager' }),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
canEditTaskByIdentity(task, { currentUserId: 'execution-owner', projectManagerUserId: 'project-manager' }),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
canEditTaskByIdentity(task, { currentUserId: 'project-manager', projectManagerUserId: 'project-manager' }),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not allow unrelated users to edit just because they have a task permission code', () => {
|
||||||
|
const task = createTask();
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
canEditTaskByIdentity(task, {
|
||||||
|
currentUserId: 'developer',
|
||||||
|
projectManagerUserId: 'project-manager'
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the same identity rule for delete and only blocks completed tasks by status', () => {
|
||||||
|
assert.equal(
|
||||||
|
canDeleteTaskByIdentity(createTask({ statusCode: 'active' }), {
|
||||||
|
currentUserId: 'execution-owner',
|
||||||
|
projectManagerUserId: 'project-manager'
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
canDeleteTaskByIdentity(createTask({ statusCode: 'completed' }), {
|
||||||
|
currentUserId: 'execution-owner',
|
||||||
|
projectManagerUserId: 'project-manager'
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('execution mutation permission rules', () => {
|
||||||
|
it('allows only execution owner and project manager to edit mutable executions', () => {
|
||||||
|
const execution = createExecution();
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
canEditExecutionByIdentity(execution, {
|
||||||
|
currentUserId: 'execution-owner',
|
||||||
|
projectManagerUserId: 'project-manager'
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
canEditExecutionByIdentity(execution, {
|
||||||
|
currentUserId: 'project-manager',
|
||||||
|
projectManagerUserId: 'project-manager'
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
canEditExecutionByIdentity(execution, {
|
||||||
|
currentUserId: 'developer',
|
||||||
|
projectManagerUserId: 'project-manager'
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the same identity rule for delete and only blocks completed executions by status', () => {
|
||||||
|
assert.equal(
|
||||||
|
canDeleteExecutionByIdentity(createExecution({ statusCode: 'active' }), {
|
||||||
|
currentUserId: 'execution-owner',
|
||||||
|
projectManagerUserId: 'project-manager'
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
canDeleteExecutionByIdentity(createExecution({ statusCode: 'active' }), {
|
||||||
|
currentUserId: 'developer',
|
||||||
|
projectManagerUserId: 'project-manager'
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
canDeleteExecutionByIdentity(createExecution({ statusCode: 'completed' }), {
|
||||||
|
currentUserId: 'execution-owner',
|
||||||
|
projectManagerUserId: 'project-manager'
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user