ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: PROJECT-LEVEL ДОСТУПЫ OPERATIONAL CORE
This commit is contained in:
@@ -15,11 +15,13 @@ import {
|
||||
deleteAdminInvite,
|
||||
deleteAdminMembership,
|
||||
deleteAdminService,
|
||||
ensureAdminTaskManagerProjectMembership,
|
||||
ensureAdminTaskManagerWorkspaceMembership,
|
||||
fetchAdminTaskManagerWorkspaces,
|
||||
fetchControlPlaneSnapshot,
|
||||
reorderAdminServices,
|
||||
retryAdminSync,
|
||||
removeAdminTaskManagerProjectMembership,
|
||||
removeAdminTaskManagerWorkspaceMembership,
|
||||
setAdminUserServiceAccess,
|
||||
updateAdminClient,
|
||||
@@ -57,6 +59,7 @@ import {
|
||||
AdminOverlay,
|
||||
type AccessAssignmentValue,
|
||||
type CreateUserCommand,
|
||||
type EnsureTaskManagerProjectMemberCommand,
|
||||
type SetUserServiceAccessCommand,
|
||||
} from "../widgets/admin-overlay/AdminOverlay";
|
||||
import { ProfileSettingsPanel } from "../widgets/profile-settings-panel/ProfileSettingsPanel";
|
||||
@@ -87,6 +90,7 @@ export function LauncherApp() {
|
||||
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
|
||||
const [pendingAccessAssignments, setPendingAccessAssignments] = useState<Record<string, AccessAssignmentValue>>({});
|
||||
const [pendingTaskManagerMemberships, setPendingTaskManagerMemberships] = useState<Record<string, boolean>>({});
|
||||
const [pendingTaskManagerProjectMemberships, setPendingTaskManagerProjectMemberships] = useState<Record<string, boolean>>({});
|
||||
const [taskManagerWorkspaces, setTaskManagerWorkspaces] = useState<TaskManagerWorkspaceSummary[]>([]);
|
||||
const [taskManagerWorkspacesLoading, setTaskManagerWorkspacesLoading] = useState(false);
|
||||
const [taskManagerWorkspacesError, setTaskManagerWorkspacesError] = useState<string | null>(null);
|
||||
@@ -506,6 +510,45 @@ export function LauncherApp() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleSetTaskManagerProjectMemberRole(command: EnsureTaskManagerProjectMemberCommand) {
|
||||
const membershipKey = `${command.clientId}:${command.userId}:${command.workspaceSlug}:${command.projectId}`;
|
||||
|
||||
if (pendingTaskManagerProjectMemberships[membershipKey]) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingTaskManagerProjectMemberships((current) => ({ ...current, [membershipKey]: true }));
|
||||
const request =
|
||||
command.role === "unset"
|
||||
? removeAdminTaskManagerProjectMembership({
|
||||
clientId: command.clientId,
|
||||
userId: command.userId,
|
||||
workspaceSlug: command.workspaceSlug,
|
||||
projectId: command.projectId,
|
||||
})
|
||||
: ensureAdminTaskManagerProjectMembership({
|
||||
clientId: command.clientId,
|
||||
userId: command.userId,
|
||||
workspaceSlug: command.workspaceSlug,
|
||||
projectId: command.projectId,
|
||||
role: command.role,
|
||||
});
|
||||
|
||||
request
|
||||
.then((result) => {
|
||||
setData(syncLauncherServiceLinks(result.data));
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.warn(error instanceof Error ? error.message : "Не удалось синхронизировать проект Tasker");
|
||||
})
|
||||
.finally(() => {
|
||||
setPendingTaskManagerProjectMemberships((current) => {
|
||||
const { [membershipKey]: _completed, ...rest } = current;
|
||||
return rest;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateInvite(invite: Pick<Invite, "clientId" | "email" | "role">) {
|
||||
applyControlPlaneMutation(createAdminInvite(invite));
|
||||
}
|
||||
@@ -752,8 +795,10 @@ export function LauncherApp() {
|
||||
taskManagerWorkspacesLoading={taskManagerWorkspacesLoading}
|
||||
taskManagerWorkspacesError={taskManagerWorkspacesError}
|
||||
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
|
||||
pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
|
||||
onRefreshTaskManagerWorkspaces={() => void refreshTaskManagerWorkspaces()}
|
||||
onSetTaskManagerWorkspaceMemberRole={handleSetTaskManagerWorkspaceMemberRole}
|
||||
onSetTaskManagerProjectMemberRole={handleSetTaskManagerProjectMemberRole}
|
||||
/>
|
||||
) : null}
|
||||
{profileSettingsOpen && activeProfileUser ? (
|
||||
|
||||
@@ -37,6 +37,15 @@ export interface TaskManagerWorkspaceSummary {
|
||||
name: string;
|
||||
ownerEmail: string | null;
|
||||
memberCount: number;
|
||||
projects?: TaskManagerProjectSummary[];
|
||||
}
|
||||
|
||||
export interface TaskManagerProjectSummary {
|
||||
id: string;
|
||||
workspaceSlug: string;
|
||||
name: string;
|
||||
identifier: string;
|
||||
memberCount: number;
|
||||
}
|
||||
|
||||
export interface TaskManagerWorkspaceMembershipResult {
|
||||
@@ -52,6 +61,20 @@ export interface TaskManagerWorkspaceMembershipResult {
|
||||
isBanned: boolean;
|
||||
}
|
||||
|
||||
export interface TaskManagerProjectMembershipResult {
|
||||
created: boolean;
|
||||
workspace: TaskManagerWorkspaceSummary;
|
||||
project: TaskManagerProjectSummary;
|
||||
member: {
|
||||
id: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
};
|
||||
role: number;
|
||||
roleSlug: Exclude<TaskManagerWorkspaceMemberRole, "unset">;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export type TaskManagerWorkspaceMemberRole = "unset" | "guest" | "member" | "admin";
|
||||
|
||||
export interface TaskManagerWorkspaceMembershipMutationResult extends ControlPlaneMutationResult {
|
||||
@@ -74,6 +97,27 @@ export interface TaskManagerWorkspaceMembershipRemoveMutationResult extends Cont
|
||||
};
|
||||
}
|
||||
|
||||
export interface TaskManagerProjectMembershipMutationResult extends ControlPlaneMutationResult {
|
||||
taskManager: {
|
||||
ok: boolean;
|
||||
membership: TaskManagerProjectMembershipResult;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TaskManagerProjectMembershipRemoveMutationResult extends ControlPlaneMutationResult {
|
||||
taskManager: {
|
||||
ok: boolean;
|
||||
removed: boolean;
|
||||
workspace: TaskManagerWorkspaceSummary;
|
||||
project: TaskManagerProjectSummary;
|
||||
member: {
|
||||
id: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchControlPlaneSnapshot(): Promise<ControlPlaneSnapshot> {
|
||||
return requestJson<ControlPlaneSnapshot>("/api/admin/control-plane");
|
||||
}
|
||||
@@ -171,6 +215,31 @@ export async function removeAdminTaskManagerWorkspaceMembership(payload: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function ensureAdminTaskManagerProjectMembership(payload: {
|
||||
clientId: string;
|
||||
userId: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
role: Exclude<TaskManagerWorkspaceMemberRole, "unset">;
|
||||
}): Promise<TaskManagerProjectMembershipMutationResult> {
|
||||
return requestJson<TaskManagerProjectMembershipMutationResult>("/api/admin/task-manager/project-memberships/ensure", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeAdminTaskManagerProjectMembership(payload: {
|
||||
clientId: string;
|
||||
userId: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
}): Promise<TaskManagerProjectMembershipRemoveMutationResult> {
|
||||
return requestJson<TaskManagerProjectMembershipRemoveMutationResult>("/api/admin/task-manager/project-memberships/remove", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function createAdminGroup(payload: Pick<ClientGroup, "clientId"> & Partial<ClientGroup>): Promise<ControlPlaneMutationResult> {
|
||||
return requestJson<ControlPlaneMutationResult>("/api/admin/groups", {
|
||||
method: "POST",
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface LauncherData {
|
||||
syncStatuses: SyncStatus[];
|
||||
auditEvents: typeof mockAuditEvents;
|
||||
taskManagerMemberships: TaskManagerMembershipAssignment[];
|
||||
taskManagerProjectMemberships: TaskManagerProjectMembershipAssignment[];
|
||||
settings: LauncherSettings;
|
||||
}
|
||||
|
||||
@@ -76,6 +77,21 @@ export interface TaskManagerMembershipAssignment {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TaskManagerProjectMembershipAssignment {
|
||||
id: string;
|
||||
clientId: string;
|
||||
userId: string;
|
||||
workspaceSlug: string;
|
||||
workspaceName?: string | null;
|
||||
projectId: string;
|
||||
projectIdentifier?: string | null;
|
||||
projectName?: string | null;
|
||||
role: "guest" | "member" | "admin";
|
||||
planeUserId?: string | null;
|
||||
planeRole?: number | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface LauncherSettings {
|
||||
brand: {
|
||||
logoLinkUrl: string;
|
||||
@@ -174,6 +190,7 @@ export function normalizeLauncherData(data: Partial<LauncherData> | null | undef
|
||||
syncStatuses: Array.isArray(payload.syncStatuses) ? payload.syncStatuses : mockSyncStatuses,
|
||||
auditEvents: Array.isArray(payload.auditEvents) ? payload.auditEvents : mockAuditEvents,
|
||||
taskManagerMemberships: Array.isArray(payload.taskManagerMemberships) ? payload.taskManagerMemberships : [],
|
||||
taskManagerProjectMemberships: Array.isArray(payload.taskManagerProjectMemberships) ? payload.taskManagerProjectMemberships : [],
|
||||
settings: normalizeLauncherSettings(payload.settings),
|
||||
};
|
||||
}
|
||||
|
||||
+75
-5
@@ -3215,17 +3215,38 @@ code {
|
||||
|
||||
.task-access-modal {
|
||||
width: min(44rem, calc(100vw - 2rem));
|
||||
max-height: min(44rem, calc(100vh - 2.8rem));
|
||||
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.task-access-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.task-access-summary .info-line {
|
||||
min-width: 0;
|
||||
min-height: 3.65rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-access-summary .info-line strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-workspace-access-list {
|
||||
display: grid;
|
||||
min-height: 0;
|
||||
align-content: start;
|
||||
gap: 0.7rem;
|
||||
overflow: auto;
|
||||
padding-right: 0.15rem;
|
||||
}
|
||||
|
||||
.task-workspace-access-card {
|
||||
@@ -3240,13 +3261,21 @@ code {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 10.8rem;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.task-workspace-access-card__head > div {
|
||||
min-width: 0;
|
||||
padding-top: 0.08rem;
|
||||
}
|
||||
|
||||
.task-workspace-access-card__head strong,
|
||||
.task-workspace-access-card__head small {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-workspace-access-card__head small {
|
||||
@@ -3255,25 +3284,66 @@ code {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.task-project-access-note {
|
||||
.task-project-access-list {
|
||||
display: grid;
|
||||
gap: 0.22rem;
|
||||
gap: 0.55rem;
|
||||
padding: 0.7rem;
|
||||
border-radius: 0.85rem;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.task-project-access-note strong {
|
||||
.task-project-access-list__head {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.task-project-access-list__head strong {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.task-project-access-note span {
|
||||
.task-project-access-list__head span {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.task-project-access-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 10.8rem;
|
||||
gap: 0.65rem;
|
||||
align-items: center;
|
||||
min-height: 3.4rem;
|
||||
padding: 0.55rem;
|
||||
border-radius: 0.85rem;
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
}
|
||||
|
||||
.task-project-access-row__meta {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.16rem;
|
||||
}
|
||||
|
||||
.task-project-access-row__meta strong,
|
||||
.task-project-access-row__meta small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-project-access-row__meta strong {
|
||||
color: var(--text-primary);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.task-project-access-row__meta small {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.access-explanation {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user