ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: PROJECT-LEVEL ДОСТУПЫ OPERATIONAL CORE

This commit is contained in:
DCCONSTRUCTIONS
2026-05-08 15:19:38 +03:00
parent 652a6ef0c5
commit 11dd8d1043
8 changed files with 782 additions and 34 deletions
+126 -6
View File
@@ -66,7 +66,7 @@ import {
type MeResponse,
type TaskManagerWorkspaceCreationPolicy,
} from "../../shared/api/mockApi";
import type { TaskManagerWorkspaceMemberRole, TaskManagerWorkspaceSummary } from "../../shared/api/adminApi";
import type { TaskManagerProjectSummary, TaskManagerWorkspaceMemberRole, TaskManagerWorkspaceSummary } from "../../shared/api/adminApi";
import { uploadStorageFile } from "../../shared/api/storageApi";
import { cn } from "../../shared/lib/cn";
import { formatDate, formatDateTime } from "../../shared/lib/format";
@@ -114,6 +114,14 @@ export interface EnsureTaskManagerWorkspaceMemberCommand {
role: TaskManagerWorkspaceMemberRole;
}
export interface EnsureTaskManagerProjectMemberCommand {
clientId: string;
userId: string;
workspaceSlug: string;
projectId: string;
role: TaskManagerWorkspaceMemberRole;
}
const rootSections: Array<{ id: AdminSection; label: string; icon: React.ReactNode }> = [
{ id: "overview", label: "Обзор", icon: <LayoutDashboard size={16} /> },
{ id: "clients", label: "Клиенты", icon: <Building2 size={16} /> },
@@ -166,8 +174,10 @@ export function AdminOverlay({
taskManagerWorkspacesLoading,
taskManagerWorkspacesError,
pendingTaskManagerMemberships,
pendingTaskManagerProjectMemberships,
onRefreshTaskManagerWorkspaces,
onSetTaskManagerWorkspaceMemberRole,
onSetTaskManagerProjectMemberRole,
}: {
data: LauncherData;
me: MeResponse;
@@ -198,8 +208,10 @@ export function AdminOverlay({
taskManagerWorkspacesLoading: boolean;
taskManagerWorkspacesError: string | null;
pendingTaskManagerMemberships: Record<string, boolean>;
pendingTaskManagerProjectMemberships: Record<string, boolean>;
onRefreshTaskManagerWorkspaces: () => void;
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
}) {
const isRoot = me.launcherRole === "root_admin";
const sections = isRoot ? rootSections : clientSections;
@@ -375,7 +387,10 @@ export function AdminOverlay({
onUpdateUser={onUpdateUser}
onUpdateMembership={onUpdateMembership}
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
taskManagerWorkspaceCatalog={taskManagerWorkspaces}
onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
onSetTaskManagerProjectMemberRole={onSetTaskManagerProjectMemberRole}
/>
) : null}
{activeSection === "invites" ? (
@@ -948,6 +963,14 @@ const operationalCoreRoleOptions: Array<NodeDcSelectOption<OperationalCoreRoleSe
{ value: "pending", label: "Сохраняем...", disabled: true, hidden: true },
];
const taskManagerProjectRoleOptions: Array<NodeDcSelectOption<OperationalCoreRoleSelectValue>> = [
{ value: "unset", label: "—", description: "Не назначен" },
{ value: "viewer", label: "Гость", description: "Просмотр", tone: "green" },
{ value: "member", label: "Участник", description: "Рабочий доступ", tone: "green" },
{ value: "admin", label: "Админ", description: "Администрирование", tone: "green" },
{ value: "pending", label: "Сохраняем...", disabled: true, hidden: true },
];
function membershipRoleLabel(role: ClientMembershipRole): string {
return membershipRoleOptions.find((option) => option.value === role)?.label ?? role;
}
@@ -1005,6 +1028,28 @@ function getTaskManagerMembershipRole(data: LauncherData, clientId: string, user
return data.taskManagerMemberships.find((membership) => membership.clientId === clientId && membership.userId === userId && membership.workspaceSlug === workspaceSlug)?.role ?? "unset";
}
function getTaskManagerProjectMembershipRole(
data: LauncherData,
clientId: string,
userId: string,
workspaceSlug: string,
projectId: string
): TaskManagerWorkspaceMemberRole {
return (
data.taskManagerProjectMemberships.find(
(membership) =>
membership.clientId === clientId &&
membership.userId === userId &&
membership.workspaceSlug === workspaceSlug &&
membership.projectId === projectId
)?.role ?? "unset"
);
}
function getWorkspaceCatalogProjects(workspace: ClientTaskManagerWorkspaceBinding, catalog: TaskManagerWorkspaceSummary[]): TaskManagerProjectSummary[] {
return catalog.find((item) => item.slug === workspace.slug)?.projects ?? [];
}
function normalizeClientTaskManagerWorkspaceDraft(workspaces: ClientTaskManagerWorkspaceBinding[]): ClientTaskManagerWorkspaceBinding[] {
const bySlug = new Map<string, ClientTaskManagerWorkspaceBinding>();
@@ -2228,7 +2273,10 @@ function AccessSection({
onUpdateUser,
onUpdateMembership,
pendingTaskManagerMemberships,
pendingTaskManagerProjectMemberships,
taskManagerWorkspaceCatalog,
onSetTaskManagerWorkspaceMemberRole,
onSetTaskManagerProjectMemberRole,
}: {
data: LauncherData;
matrix: ReturnType<typeof buildAccessMatrix>;
@@ -2239,10 +2287,13 @@ function AccessSection({
onUpdateUser: (userId: string, patch: Partial<LauncherUser>) => void;
onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
pendingTaskManagerMemberships: Record<string, boolean>;
pendingTaskManagerProjectMemberships: Record<string, boolean>;
taskManagerWorkspaceCatalog: TaskManagerWorkspaceSummary[];
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
}) {
const hasUsers = matrix.users.length > 0;
const taskManagerWorkspaces = getClientTaskManagerWorkspaces(matrix.client);
const clientTaskManagerWorkspaces = getClientTaskManagerWorkspaces(matrix.client);
const primaryTaskManagerWorkspace = getPrimaryTaskManagerWorkspace(matrix.client);
const [detailsCell, setDetailsCell] = useState<AccessMatrixCell | null>(null);
@@ -2399,12 +2450,15 @@ function AccessSection({
user={getUser(data, detailsCell.userId)}
service={getService(data, detailsCell.serviceId)}
cell={detailsCell}
workspaces={taskManagerWorkspaces}
workspaces={clientTaskManagerWorkspaces}
workspaceCatalog={taskManagerWorkspaceCatalog}
pendingAccessAssignments={pendingAccessAssignments}
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
onClose={() => setDetailsCell(null)}
onSetUserServiceAccess={onSetUserServiceAccess}
onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
onSetTaskManagerProjectMemberRole={onSetTaskManagerProjectMemberRole}
/>
) : null}
</div>
@@ -2501,11 +2555,14 @@ function OperationalCoreAccessModal({
service,
cell,
workspaces,
workspaceCatalog,
pendingAccessAssignments,
pendingTaskManagerMemberships,
pendingTaskManagerProjectMemberships,
onClose,
onSetUserServiceAccess,
onSetTaskManagerWorkspaceMemberRole,
onSetTaskManagerProjectMemberRole,
}: {
data: LauncherData;
client: Client;
@@ -2513,11 +2570,14 @@ function OperationalCoreAccessModal({
service: Service;
cell: AccessMatrixCell;
workspaces: ClientTaskManagerWorkspaceBinding[];
workspaceCatalog: TaskManagerWorkspaceSummary[];
pendingAccessAssignments: Record<string, AccessAssignmentValue>;
pendingTaskManagerMemberships: Record<string, boolean>;
pendingTaskManagerProjectMemberships: Record<string, boolean>;
onClose: () => void;
onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
}) {
const membership = data.memberships.find((item) => item.clientId === client.id && item.userId === user.id);
const protectedUser = user.id === "user_root" || membership?.role === "client_owner";
@@ -2538,6 +2598,7 @@ function OperationalCoreAccessModal({
{workspaces.length ? (
workspaces.map((workspace) => {
const role = getTaskManagerMembershipRole(data, client.id, user.id, workspace.slug);
const projects = getWorkspaceCatalogProjects(workspace, workspaceCatalog);
const pendingKey = `${client.id}:${user.id}:${workspace.slug}`;
const pending = Boolean(pendingTaskManagerMemberships[pendingKey]) || basePendingValue !== undefined;
const value: OperationalCoreRoleSelectValue = pending
@@ -2580,9 +2641,68 @@ function OperationalCoreAccessModal({
/>
)}
</div>
<div className="task-project-access-note">
<strong>Проекты</strong>
<span>Сейчас наследуют роль workspace. Точечные роли проектов подключаются следующим Project API-адаптером.</span>
<div className="task-project-access-list">
<div className="task-project-access-list__head">
<strong>Проекты</strong>
<span>{projects.length ? "Точечные роли внутри выбранного workspace" : "В этом workspace пока нет проектов"}</span>
</div>
{projects.map((project) => {
const projectRole = getTaskManagerProjectMembershipRole(data, client.id, user.id, workspace.slug, project.id);
const projectPendingKey = `${client.id}:${user.id}:${workspace.slug}:${project.id}`;
const projectPending = Boolean(pendingTaskManagerProjectMemberships[projectPendingKey]);
const projectValue: OperationalCoreRoleSelectValue = projectPending ? "pending" : taskManagerRoleToAccessAssignment(projectRole);
return (
<div key={project.id} className="task-project-access-row">
<div className="task-project-access-row__meta">
<strong>{project.name}</strong>
<small>
{project.identifier}
{project.memberCount ? ` · ${project.memberCount} участников` : ""}
</small>
</div>
{protectedUser ? (
<AdminStaticPill>{accessAssignmentLabel("admin")}</AdminStaticPill>
) : (
<NodeDcSelect
className="admin-table-select-wrap"
triggerClassName="admin-table-select-trigger"
value={projectValue}
options={taskManagerProjectRoleOptions}
label={`Роль ${user.name} в проекте ${project.name}`}
minMenuWidth={180}
disabled={projectPending}
onChange={(nextValue) => {
if (nextValue === "pending") return;
const nextTaskManagerRole = accessAssignmentToTaskManagerRole(nextValue);
if (nextTaskManagerRole !== "unset") {
if (baseAssignmentValue === "unset" || baseAssignmentValue === "deny") {
onSetUserServiceAccess({ userId: user.id, serviceId: service.id, value: nextValue });
}
if (role === "unset") {
onSetTaskManagerWorkspaceMemberRole({
clientId: client.id,
userId: user.id,
workspaceSlug: workspace.slug,
role: nextTaskManagerRole,
});
}
}
onSetTaskManagerProjectMemberRole({
clientId: client.id,
userId: user.id,
workspaceSlug: workspace.slug,
projectId: project.id,
role: nextTaskManagerRole,
});
}}
/>
)}
</div>
);
})}
</div>
</section>
);