ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: PROJECT-LEVEL ДОСТУПЫ OPERATIONAL CORE
This commit is contained in:
parent
652a6ef0c5
commit
11dd8d1043
|
|
@ -14,11 +14,23 @@
|
||||||
"contactEmail": "dcctouch@gmail.com",
|
"contactEmail": "dcctouch@gmail.com",
|
||||||
"notes": "Live-клиент NODE.DC для первичной проверки control-plane, SSO и доступа к сервисам.",
|
"notes": "Live-клиент NODE.DC для первичной проверки control-plane, SSO и доступа к сервисам.",
|
||||||
"createdAt": "2026-05-04T00:00:00.000Z",
|
"createdAt": "2026-05-04T00:00:00.000Z",
|
||||||
"updatedAt": "2026-05-06T08:44:44.882Z",
|
"updatedAt": "2026-05-08T10:14:10.727Z",
|
||||||
"integrations": {
|
"integrations": {
|
||||||
"taskManager": {
|
"taskManager": {
|
||||||
"workspaceSlug": "nodedc",
|
"workspaceSlug": "nodedc",
|
||||||
"workspaceName": "NODE DC"
|
"workspaceName": "NODE DC",
|
||||||
|
"workspaces": [
|
||||||
|
{
|
||||||
|
"slug": "nodedc",
|
||||||
|
"name": "NODE DC",
|
||||||
|
"isPrimary": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "dcabramov",
|
||||||
|
"name": "DCABRAMOV",
|
||||||
|
"isPrimary": false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"inn": null
|
"inn": null
|
||||||
|
|
@ -75,7 +87,7 @@
|
||||||
"avatarUrl": null,
|
"avatarUrl": null,
|
||||||
"globalStatus": "active",
|
"globalStatus": "active",
|
||||||
"createdAt": "2026-05-05T16:02:43.235Z",
|
"createdAt": "2026-05-05T16:02:43.235Z",
|
||||||
"updatedAt": "2026-05-05T16:04:53.004Z"
|
"updatedAt": "2026-05-08T11:44:25.215Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "user_silverpsih007_gmail_com",
|
"id": "user_silverpsih007_gmail_com",
|
||||||
|
|
@ -149,10 +161,10 @@
|
||||||
"id": "mem_client_romashka_support_dctouch_ru",
|
"id": "mem_client_romashka_support_dctouch_ru",
|
||||||
"clientId": "client_romashka",
|
"clientId": "client_romashka",
|
||||||
"userId": "user_support_dctouch_ru",
|
"userId": "user_support_dctouch_ru",
|
||||||
"role": "member",
|
"role": "client_admin",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"createdAt": "2026-05-05T16:02:43.235Z",
|
"createdAt": "2026-05-05T16:02:43.235Z",
|
||||||
"updatedAt": "2026-05-05T16:02:43.235Z"
|
"updatedAt": "2026-05-08T11:44:24.773Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "mem_client_romashka_silverpsih007_gmail_com",
|
"id": "mem_client_romashka_silverpsih007_gmail_com",
|
||||||
|
|
@ -387,16 +399,6 @@
|
||||||
"createdAt": "2026-05-05T14:57:13.249Z",
|
"createdAt": "2026-05-05T14:57:13.249Z",
|
||||||
"updatedAt": "2026-05-05T14:57:13.249Z"
|
"updatedAt": "2026-05-05T14:57:13.249Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "grant_task_manager_user_support_dctouch_ru",
|
|
||||||
"serviceId": "service_task_manager",
|
|
||||||
"targetType": "user",
|
|
||||||
"targetId": "user_support_dctouch_ru",
|
|
||||||
"appRole": "member",
|
|
||||||
"status": "active",
|
|
||||||
"createdAt": "2026-05-05T16:04:52.709Z",
|
|
||||||
"updatedAt": "2026-05-05T16:04:52.709Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "grant_task_manager_user_silverpsih007_gmail_com",
|
"id": "grant_task_manager_user_silverpsih007_gmail_com",
|
||||||
"serviceId": "service_task_manager",
|
"serviceId": "service_task_manager",
|
||||||
|
|
@ -426,6 +428,16 @@
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"createdAt": "2026-05-06T01:28:06.515Z",
|
"createdAt": "2026-05-06T01:28:06.515Z",
|
||||||
"updatedAt": "2026-05-06T01:28:06.515Z"
|
"updatedAt": "2026-05-06T01:28:06.515Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grant_task_manager_user_support_dctouch_ru",
|
||||||
|
"serviceId": "service_task_manager",
|
||||||
|
"targetType": "user",
|
||||||
|
"targetId": "user_support_dctouch_ru",
|
||||||
|
"appRole": "member",
|
||||||
|
"status": "active",
|
||||||
|
"createdAt": "2026-05-05T16:04:52.709Z",
|
||||||
|
"updatedAt": "2026-05-08T10:14:37.303Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"exceptions": [],
|
"exceptions": [],
|
||||||
|
|
@ -501,7 +513,7 @@
|
||||||
"state": "pending",
|
"state": "pending",
|
||||||
"lastSyncAt": "2026-05-04T12:55:13.842Z",
|
"lastSyncAt": "2026-05-04T12:55:13.842Z",
|
||||||
"error": null,
|
"error": null,
|
||||||
"updatedAt": "2026-05-06T08:44:44.887Z"
|
"updatedAt": "2026-05-08T10:14:10.729Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sync_dc_touch_authentik",
|
"id": "sync_dc_touch_authentik",
|
||||||
|
|
@ -620,9 +632,9 @@
|
||||||
"objectType": "user",
|
"objectType": "user",
|
||||||
"target": "authentik",
|
"target": "authentik",
|
||||||
"state": "synced",
|
"state": "synced",
|
||||||
"lastSyncAt": "2026-05-05T16:04:53.004Z",
|
"lastSyncAt": "2026-05-08T11:44:25.215Z",
|
||||||
"error": null,
|
"error": null,
|
||||||
"updatedAt": "2026-05-05T16:04:53.004Z"
|
"updatedAt": "2026-05-08T11:44:25.215Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sync_grant_service_task_manager_user_support_dctouch_ru",
|
"id": "sync_grant_service_task_manager_user_support_dctouch_ru",
|
||||||
|
|
@ -633,7 +645,7 @@
|
||||||
"state": "pending",
|
"state": "pending",
|
||||||
"lastSyncAt": null,
|
"lastSyncAt": null,
|
||||||
"error": null,
|
"error": null,
|
||||||
"updatedAt": "2026-05-05T16:04:52.709Z"
|
"updatedAt": "2026-05-08T10:14:37.305Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sync_invite_invite_silverpsih007_gmail_com",
|
"id": "sync_invite_invite_silverpsih007_gmail_com",
|
||||||
|
|
@ -2367,6 +2379,126 @@
|
||||||
"clientId": null,
|
"clientId": null,
|
||||||
"result": "success",
|
"result": "success",
|
||||||
"details": "Groups: nodedc:launcher:user, nodedc:taskmanager:user"
|
"details": "Groups: nodedc:launcher:user, nodedc:taskmanager:user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_support_2",
|
||||||
|
"at": "2026-05-08T10:08:56.801Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker project",
|
||||||
|
"objectType": "task-manager-project-membership",
|
||||||
|
"objectName": "DC SUPPORT",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Project: DCTM-WT-CODEX; Role: member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dctouch_2",
|
||||||
|
"at": "2026-05-08T10:10:17.084Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Обновлён клиент",
|
||||||
|
"objectType": "client",
|
||||||
|
"objectName": "DCTOUCH",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dctouch_3",
|
||||||
|
"at": "2026-05-08T10:11:08.178Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Обновлён клиент",
|
||||||
|
"objectType": "client",
|
||||||
|
"objectName": "DCTOUCH",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dctouch_4",
|
||||||
|
"at": "2026-05-08T10:14:10.729Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Обновлён клиент",
|
||||||
|
"objectType": "client",
|
||||||
|
"objectName": "DCTOUCH",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_support_dctouch_ru_task_manager_2",
|
||||||
|
"at": "2026-05-08T10:14:37.304Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Обновлён доступ пользователя к сервису",
|
||||||
|
"objectType": "grant",
|
||||||
|
"objectName": "support@dctouch.ru / task-manager",
|
||||||
|
"clientId": null,
|
||||||
|
"result": "success",
|
||||||
|
"details": "Value: member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_support_3",
|
||||||
|
"at": "2026-05-08T10:14:37.448Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC SUPPORT",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: DCABRAMOV; Role: member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_support_dctouch_ru_6",
|
||||||
|
"at": "2026-05-08T10:14:38.013Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Пользователь синхронизирован в Authentik",
|
||||||
|
"objectType": "user",
|
||||||
|
"objectName": "support@dctouch.ru",
|
||||||
|
"clientId": null,
|
||||||
|
"result": "success",
|
||||||
|
"details": "Groups: nodedc:launcher:user, nodedc:taskmanager:user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_support_dctouch_ru_7",
|
||||||
|
"at": "2026-05-08T11:44:24.774Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Обновлено членство",
|
||||||
|
"objectType": "user",
|
||||||
|
"objectName": "support@dctouch.ru",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Role: client_admin; status: active"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_support_dctouch_ru_8",
|
||||||
|
"at": "2026-05-08T11:44:25.215Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Пользователь синхронизирован в Authentik",
|
||||||
|
"objectType": "user",
|
||||||
|
"objectName": "support@dctouch.ru",
|
||||||
|
"clientId": null,
|
||||||
|
"result": "success",
|
||||||
|
"details": "Groups: nodedc:launcher:user, nodedc:taskmanager:user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_constrictions_2",
|
||||||
|
"at": "2026-05-08T11:52:50.107Z",
|
||||||
|
"actorUserId": "user_support_dctouch_ru",
|
||||||
|
"actorName": "DC SUPPORT",
|
||||||
|
"action": "Назначен Tasker project",
|
||||||
|
"objectType": "task-manager-project-membership",
|
||||||
|
"objectName": "DC CONSTRICTIONS",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Project: Менеджмент; Role: member"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|
@ -2454,6 +2586,47 @@
|
||||||
"planeUserId": "1cc7ae3a-1f42-41ac-8cc2-1ff0fce59554",
|
"planeUserId": "1cc7ae3a-1f42-41ac-8cc2-1ff0fce59554",
|
||||||
"planeRole": 15,
|
"planeRole": 15,
|
||||||
"updatedAt": "2026-05-06T11:20:48.841Z"
|
"updatedAt": "2026-05-06T11:20:48.841Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tasker_mem_client_romashka_user_support_dctouch_ru_dcabramov",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"userId": "user_support_dctouch_ru",
|
||||||
|
"workspaceSlug": "dcabramov",
|
||||||
|
"workspaceName": "DCABRAMOV",
|
||||||
|
"role": "member",
|
||||||
|
"planeUserId": "53e56870-8772-4dfd-9c8c-2eeacfb53caa",
|
||||||
|
"planeRole": 15,
|
||||||
|
"updatedAt": "2026-05-08T10:14:37.448Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"taskManagerProjectMemberships": [
|
||||||
|
{
|
||||||
|
"id": "tasker_project_mem_client_romashka_user_support_dctouch_ru_nodedc_823af409_bcfd_498c_b2fb_31119ff23",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"userId": "user_support_dctouch_ru",
|
||||||
|
"workspaceSlug": "nodedc",
|
||||||
|
"workspaceName": "NODE DC",
|
||||||
|
"projectId": "823af409-bcfd-498c-b2fb-31119ff238a7",
|
||||||
|
"projectIdentifier": "CODEX",
|
||||||
|
"projectName": "DCTM-WT-CODEX",
|
||||||
|
"role": "member",
|
||||||
|
"planeUserId": "53e56870-8772-4dfd-9c8c-2eeacfb53caa",
|
||||||
|
"planeRole": 15,
|
||||||
|
"updatedAt": "2026-05-08T10:08:56.800Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tasker_project_mem_client_romashka_user_support_dcconstructions_ru_nodedc_a3de175a_2df5_4604_aadf_6",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"userId": "user_support_dcconstructions_ru",
|
||||||
|
"workspaceSlug": "nodedc",
|
||||||
|
"workspaceName": "NODE DC",
|
||||||
|
"projectId": "a3de175a-2df5-4604-aadf-618877445135",
|
||||||
|
"projectIdentifier": "MGR",
|
||||||
|
"projectName": "Менеджмент",
|
||||||
|
"role": "member",
|
||||||
|
"planeUserId": "1cc7ae3a-1f42-41ac-8cc2-1ff0fce59554",
|
||||||
|
"planeRole": 15,
|
||||||
|
"updatedAt": "2026-05-08T11:52:50.104Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const collectionKeys = [
|
||||||
"syncStatuses",
|
"syncStatuses",
|
||||||
"auditEvents",
|
"auditEvents",
|
||||||
"taskManagerMemberships",
|
"taskManagerMemberships",
|
||||||
|
"taskManagerProjectMemberships",
|
||||||
];
|
];
|
||||||
|
|
||||||
const clientTypes = new Set(["company", "person"]);
|
const clientTypes = new Set(["company", "person"]);
|
||||||
|
|
@ -201,6 +202,9 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||||
data.taskManagerMemberships = data.taskManagerMemberships.filter(
|
data.taskManagerMemberships = data.taskManagerMemberships.filter(
|
||||||
(membership) => !(membership.clientId === client.id && membership.userId === user.id && membership.workspaceSlug === workspaceSlug)
|
(membership) => !(membership.clientId === client.id && membership.userId === user.id && membership.workspaceSlug === workspaceSlug)
|
||||||
);
|
);
|
||||||
|
data.taskManagerProjectMemberships = data.taskManagerProjectMemberships.filter(
|
||||||
|
(membership) => !(membership.clientId === client.id && membership.userId === user.id && membership.workspaceSlug === workspaceSlug)
|
||||||
|
);
|
||||||
|
|
||||||
addAuditEvent(data, actor, {
|
addAuditEvent(data, actor, {
|
||||||
action: "Снят Tasker workspace",
|
action: "Снят Tasker workspace",
|
||||||
|
|
@ -215,6 +219,73 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||||
return { data };
|
return { data };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function recordTaskManagerProjectMembership(payload, identity) {
|
||||||
|
const data = readData();
|
||||||
|
const actor = resolveActor(data, identity);
|
||||||
|
const client = findById(data.clients, payload?.clientId, "client");
|
||||||
|
const user = findById(data.users, payload?.userId, "user");
|
||||||
|
const taskManager = typeof payload?.taskManager === "object" && payload.taskManager !== null ? payload.taskManager : {};
|
||||||
|
const membership = typeof taskManager.membership === "object" && taskManager.membership !== null ? taskManager.membership : {};
|
||||||
|
const workspace = typeof membership.workspace === "object" && membership.workspace !== null ? membership.workspace : {};
|
||||||
|
const project = typeof membership.project === "object" && membership.project !== null ? membership.project : {};
|
||||||
|
|
||||||
|
upsertTaskManagerProjectMembership(data, {
|
||||||
|
clientId: client.id,
|
||||||
|
userId: user.id,
|
||||||
|
workspaceSlug: workspace.slug ?? payload?.workspaceSlug,
|
||||||
|
workspaceName: workspace.name ?? payload?.workspaceName ?? null,
|
||||||
|
projectId: project.id ?? payload?.projectId,
|
||||||
|
projectIdentifier: project.identifier ?? payload?.projectIdentifier ?? null,
|
||||||
|
projectName: project.name ?? payload?.projectName ?? null,
|
||||||
|
role: normalizeTaskManagerMembershipRole(payload?.role),
|
||||||
|
planeUserId: membership.member?.id ?? null,
|
||||||
|
planeRole: typeof membership.role === "number" ? membership.role : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
addAuditEvent(data, actor, {
|
||||||
|
action: "Назначен Tasker project",
|
||||||
|
objectType: "task-manager-project-membership",
|
||||||
|
objectName: user.name,
|
||||||
|
clientId: client.id,
|
||||||
|
result: "success",
|
||||||
|
details: `Workspace: ${workspace.name ?? workspace.slug ?? payload?.workspaceSlug}; Project: ${project.name ?? project.identifier ?? payload?.projectId}; Role: ${payload?.role ?? "member"}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeData(data);
|
||||||
|
return { data };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTaskManagerProjectMembership(payload, identity) {
|
||||||
|
const data = readData();
|
||||||
|
const actor = resolveActor(data, identity);
|
||||||
|
const client = findById(data.clients, payload?.clientId, "client");
|
||||||
|
const user = findById(data.users, payload?.userId, "user");
|
||||||
|
const workspaceSlug = requireString(payload?.workspaceSlug ?? client.integrations?.taskManager?.workspaceSlug, "workspaceSlug");
|
||||||
|
const projectId = requireString(payload?.projectId, "projectId");
|
||||||
|
|
||||||
|
data.taskManagerProjectMemberships = data.taskManagerProjectMemberships.filter(
|
||||||
|
(membership) =>
|
||||||
|
!(
|
||||||
|
membership.clientId === client.id &&
|
||||||
|
membership.userId === user.id &&
|
||||||
|
membership.workspaceSlug === workspaceSlug &&
|
||||||
|
membership.projectId === projectId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
addAuditEvent(data, actor, {
|
||||||
|
action: "Снят Tasker project",
|
||||||
|
objectType: "task-manager-project-membership",
|
||||||
|
objectName: user.name,
|
||||||
|
clientId: client.id,
|
||||||
|
result: "success",
|
||||||
|
details: `Workspace: ${workspaceSlug}; Project: ${projectId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeData(data);
|
||||||
|
return { data };
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteClient(clientId, identity) {
|
async function deleteClient(clientId, identity) {
|
||||||
const data = readData();
|
const data = readData();
|
||||||
const actor = resolveActor(data, identity);
|
const actor = resolveActor(data, identity);
|
||||||
|
|
@ -1089,7 +1160,9 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||||
reorderServices,
|
reorderServices,
|
||||||
retrySync,
|
retrySync,
|
||||||
markUserAuthentikProvisioned,
|
markUserAuthentikProvisioned,
|
||||||
|
recordTaskManagerProjectMembership,
|
||||||
recordTaskManagerWorkspaceMembership,
|
recordTaskManagerWorkspaceMembership,
|
||||||
|
removeTaskManagerProjectMembership,
|
||||||
removeTaskManagerWorkspaceMembership,
|
removeTaskManagerWorkspaceMembership,
|
||||||
setUserServiceAccess,
|
setUserServiceAccess,
|
||||||
updateClient,
|
updateClient,
|
||||||
|
|
@ -1233,6 +1306,42 @@ function upsertTaskManagerMembership(data, payload) {
|
||||||
return nextMembership;
|
return nextMembership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function upsertTaskManagerProjectMembership(data, payload) {
|
||||||
|
const workspaceSlug = requireString(payload.workspaceSlug, "workspaceSlug");
|
||||||
|
const projectId = requireString(payload.projectId, "projectId");
|
||||||
|
const existingMembership = data.taskManagerProjectMemberships.find(
|
||||||
|
(membership) =>
|
||||||
|
membership.clientId === payload.clientId &&
|
||||||
|
membership.userId === payload.userId &&
|
||||||
|
membership.workspaceSlug === workspaceSlug &&
|
||||||
|
membership.projectId === projectId
|
||||||
|
);
|
||||||
|
const nextMembership = {
|
||||||
|
id:
|
||||||
|
existingMembership?.id ??
|
||||||
|
uniqueId(data.taskManagerProjectMemberships, "tasker_project_mem", `${payload.clientId}-${payload.userId}-${workspaceSlug}-${projectId}`),
|
||||||
|
clientId: payload.clientId,
|
||||||
|
userId: payload.userId,
|
||||||
|
workspaceSlug,
|
||||||
|
workspaceName: nullableStringWithFallback(payload.workspaceName, existingMembership?.workspaceName ?? null),
|
||||||
|
projectId,
|
||||||
|
projectIdentifier: nullableStringWithFallback(payload.projectIdentifier, existingMembership?.projectIdentifier ?? null),
|
||||||
|
projectName: nullableStringWithFallback(payload.projectName, existingMembership?.projectName ?? null),
|
||||||
|
role: normalizeTaskManagerMembershipRole(payload.role),
|
||||||
|
planeUserId: nullableStringWithFallback(payload.planeUserId, existingMembership?.planeUserId ?? null),
|
||||||
|
planeRole: typeof payload.planeRole === "number" ? payload.planeRole : (existingMembership?.planeRole ?? null),
|
||||||
|
updatedAt: isoNow(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingMembership) {
|
||||||
|
Object.assign(existingMembership, nextMembership);
|
||||||
|
return existingMembership;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.taskManagerProjectMemberships.push(nextMembership);
|
||||||
|
return nextMembership;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveActor(data, identity) {
|
function resolveActor(data, identity) {
|
||||||
const user = data.users.find(
|
const user = data.users.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
|
|
|
||||||
|
|
@ -521,13 +521,29 @@ app.get("/api/admin/clients", requireLauncherAdmin, (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/admin/task-manager/workspaces", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
app.get("/api/admin/task-manager/workspaces", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||||
if (!req.nodedcAdminScope?.isRoot) {
|
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspaces/");
|
||||||
res.json({ ok: true, workspaces: [] });
|
|
||||||
|
if (req.nodedcAdminScope?.isRoot) {
|
||||||
|
res.json(taskManager);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspaces/");
|
const allowedWorkspaceSlugs = new Set(
|
||||||
res.json(taskManager);
|
req.nodedcAdminScope.snapshot.data.clients
|
||||||
|
.filter((client) => req.nodedcAdminScope.clientIds.has(client.id))
|
||||||
|
.flatMap((client) => {
|
||||||
|
const workspaces = Array.isArray(client.integrations?.taskManager?.workspaces)
|
||||||
|
? client.integrations.taskManager.workspaces
|
||||||
|
: [];
|
||||||
|
const slugs = workspaces.map((workspace) => workspace?.slug).filter((slug) => typeof slug === "string" && slug.trim());
|
||||||
|
const legacySlug = client.integrations?.taskManager?.workspaceSlug;
|
||||||
|
return legacySlug ? [...slugs, legacySlug] : slugs;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
res.json({
|
||||||
|
...taskManager,
|
||||||
|
workspaces: (taskManager.workspaces ?? []).filter((workspace) => allowedWorkspaceSlugs.has(workspace.slug)),
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||||
|
|
@ -667,6 +683,132 @@ app.post("/api/admin/task-manager/workspace-memberships/remove", requireLauncher
|
||||||
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
|
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
app.post("/api/admin/task-manager/project-memberships/ensure", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||||
|
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||||
|
const clientId = typeof req.body?.clientId === "string" ? req.body.clientId : "";
|
||||||
|
const userId = typeof req.body?.userId === "string" ? req.body.userId : "";
|
||||||
|
const client = snapshot.data.clients.find((candidate) => candidate.id === clientId);
|
||||||
|
const user = snapshot.data.users.find((candidate) => candidate.id === userId);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
res.status(404).json({ ok: false, error: "client_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({ ok: false, error: "user_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug);
|
||||||
|
const projectId = normalizeOptionalText(req.body?.projectId);
|
||||||
|
const role = normalizeTaskManagerRole(req.body?.role);
|
||||||
|
|
||||||
|
if (!workspaceSlug) {
|
||||||
|
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
res.status(400).json({ ok: false, error: "task_manager_project_not_configured" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
res.status(400).json({ ok: false, error: "task_manager_role_invalid" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/project-memberships/ensure/", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
email: user.email,
|
||||||
|
subject: user.authentikUserId ?? undefined,
|
||||||
|
avatarUrl: resolveUserAvatarPublicUrl(user),
|
||||||
|
role,
|
||||||
|
setLastWorkspace: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await controlPlaneStore.recordTaskManagerProjectMembership(
|
||||||
|
{
|
||||||
|
clientId: client.id,
|
||||||
|
userId: user.id,
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
role,
|
||||||
|
taskManager,
|
||||||
|
},
|
||||||
|
req.nodedcSession.user
|
||||||
|
);
|
||||||
|
|
||||||
|
publishControlPlaneEvent("admin.task-manager.project-membership.updated", [user.id]);
|
||||||
|
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.post("/api/admin/task-manager/project-memberships/remove", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||||
|
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||||
|
const clientId = typeof req.body?.clientId === "string" ? req.body.clientId : "";
|
||||||
|
const userId = typeof req.body?.userId === "string" ? req.body.userId : "";
|
||||||
|
const client = snapshot.data.clients.find((candidate) => candidate.id === clientId);
|
||||||
|
const user = snapshot.data.users.find((candidate) => candidate.id === userId);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
res.status(404).json({ ok: false, error: "client_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({ ok: false, error: "user_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug);
|
||||||
|
const projectId = normalizeOptionalText(req.body?.projectId);
|
||||||
|
|
||||||
|
if (!workspaceSlug) {
|
||||||
|
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
res.status(400).json({ ok: false, error: "task_manager_project_not_configured" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/project-memberships/remove/", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
email: user.email,
|
||||||
|
subject: user.authentikUserId ?? undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await controlPlaneStore.removeTaskManagerProjectMembership(
|
||||||
|
{
|
||||||
|
clientId: client.id,
|
||||||
|
userId: user.id,
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
},
|
||||||
|
req.nodedcSession.user
|
||||||
|
);
|
||||||
|
|
||||||
|
publishControlPlaneEvent("admin.task-manager.project-membership.updated", [user.id]);
|
||||||
|
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
|
||||||
|
}));
|
||||||
|
|
||||||
app.post("/api/admin/clients", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
app.post("/api/admin/clients", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||||
const result = await controlPlaneStore.createClient(req.body, req.nodedcSession.user);
|
const result = await controlPlaneStore.createClient(req.body, req.nodedcSession.user);
|
||||||
res.status(201).json(result);
|
res.status(201).json(result);
|
||||||
|
|
@ -2209,6 +2351,9 @@ function scopeControlPlaneData(data, scope) {
|
||||||
taskManagerMemberships: data.taskManagerMemberships.filter(
|
taskManagerMemberships: data.taskManagerMemberships.filter(
|
||||||
(membership) => clientIds.has(membership.clientId) && userIds.has(membership.userId)
|
(membership) => clientIds.has(membership.clientId) && userIds.has(membership.userId)
|
||||||
),
|
),
|
||||||
|
taskManagerProjectMemberships: data.taskManagerProjectMemberships.filter(
|
||||||
|
(membership) => clientIds.has(membership.clientId) && userIds.has(membership.userId)
|
||||||
|
),
|
||||||
syncStatuses: data.syncStatuses.filter(
|
syncStatuses: data.syncStatuses.filter(
|
||||||
(syncStatus) => clientIds.has(syncStatus.objectId) || userIds.has(syncStatus.objectId) || groupIds.has(syncStatus.objectId)
|
(syncStatus) => clientIds.has(syncStatus.objectId) || userIds.has(syncStatus.objectId) || groupIds.has(syncStatus.objectId)
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@ import {
|
||||||
deleteAdminInvite,
|
deleteAdminInvite,
|
||||||
deleteAdminMembership,
|
deleteAdminMembership,
|
||||||
deleteAdminService,
|
deleteAdminService,
|
||||||
|
ensureAdminTaskManagerProjectMembership,
|
||||||
ensureAdminTaskManagerWorkspaceMembership,
|
ensureAdminTaskManagerWorkspaceMembership,
|
||||||
fetchAdminTaskManagerWorkspaces,
|
fetchAdminTaskManagerWorkspaces,
|
||||||
fetchControlPlaneSnapshot,
|
fetchControlPlaneSnapshot,
|
||||||
reorderAdminServices,
|
reorderAdminServices,
|
||||||
retryAdminSync,
|
retryAdminSync,
|
||||||
|
removeAdminTaskManagerProjectMembership,
|
||||||
removeAdminTaskManagerWorkspaceMembership,
|
removeAdminTaskManagerWorkspaceMembership,
|
||||||
setAdminUserServiceAccess,
|
setAdminUserServiceAccess,
|
||||||
updateAdminClient,
|
updateAdminClient,
|
||||||
|
|
@ -57,6 +59,7 @@ import {
|
||||||
AdminOverlay,
|
AdminOverlay,
|
||||||
type AccessAssignmentValue,
|
type AccessAssignmentValue,
|
||||||
type CreateUserCommand,
|
type CreateUserCommand,
|
||||||
|
type EnsureTaskManagerProjectMemberCommand,
|
||||||
type SetUserServiceAccessCommand,
|
type SetUserServiceAccessCommand,
|
||||||
} from "../widgets/admin-overlay/AdminOverlay";
|
} from "../widgets/admin-overlay/AdminOverlay";
|
||||||
import { ProfileSettingsPanel } from "../widgets/profile-settings-panel/ProfileSettingsPanel";
|
import { ProfileSettingsPanel } from "../widgets/profile-settings-panel/ProfileSettingsPanel";
|
||||||
|
|
@ -87,6 +90,7 @@ export function LauncherApp() {
|
||||||
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
|
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
|
||||||
const [pendingAccessAssignments, setPendingAccessAssignments] = useState<Record<string, AccessAssignmentValue>>({});
|
const [pendingAccessAssignments, setPendingAccessAssignments] = useState<Record<string, AccessAssignmentValue>>({});
|
||||||
const [pendingTaskManagerMemberships, setPendingTaskManagerMemberships] = useState<Record<string, boolean>>({});
|
const [pendingTaskManagerMemberships, setPendingTaskManagerMemberships] = useState<Record<string, boolean>>({});
|
||||||
|
const [pendingTaskManagerProjectMemberships, setPendingTaskManagerProjectMemberships] = useState<Record<string, boolean>>({});
|
||||||
const [taskManagerWorkspaces, setTaskManagerWorkspaces] = useState<TaskManagerWorkspaceSummary[]>([]);
|
const [taskManagerWorkspaces, setTaskManagerWorkspaces] = useState<TaskManagerWorkspaceSummary[]>([]);
|
||||||
const [taskManagerWorkspacesLoading, setTaskManagerWorkspacesLoading] = useState(false);
|
const [taskManagerWorkspacesLoading, setTaskManagerWorkspacesLoading] = useState(false);
|
||||||
const [taskManagerWorkspacesError, setTaskManagerWorkspacesError] = useState<string | null>(null);
|
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">) {
|
function handleCreateInvite(invite: Pick<Invite, "clientId" | "email" | "role">) {
|
||||||
applyControlPlaneMutation(createAdminInvite(invite));
|
applyControlPlaneMutation(createAdminInvite(invite));
|
||||||
}
|
}
|
||||||
|
|
@ -752,8 +795,10 @@ export function LauncherApp() {
|
||||||
taskManagerWorkspacesLoading={taskManagerWorkspacesLoading}
|
taskManagerWorkspacesLoading={taskManagerWorkspacesLoading}
|
||||||
taskManagerWorkspacesError={taskManagerWorkspacesError}
|
taskManagerWorkspacesError={taskManagerWorkspacesError}
|
||||||
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
|
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
|
||||||
|
pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
|
||||||
onRefreshTaskManagerWorkspaces={() => void refreshTaskManagerWorkspaces()}
|
onRefreshTaskManagerWorkspaces={() => void refreshTaskManagerWorkspaces()}
|
||||||
onSetTaskManagerWorkspaceMemberRole={handleSetTaskManagerWorkspaceMemberRole}
|
onSetTaskManagerWorkspaceMemberRole={handleSetTaskManagerWorkspaceMemberRole}
|
||||||
|
onSetTaskManagerProjectMemberRole={handleSetTaskManagerProjectMemberRole}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{profileSettingsOpen && activeProfileUser ? (
|
{profileSettingsOpen && activeProfileUser ? (
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,15 @@ export interface TaskManagerWorkspaceSummary {
|
||||||
name: string;
|
name: string;
|
||||||
ownerEmail: string | null;
|
ownerEmail: string | null;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
|
projects?: TaskManagerProjectSummary[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskManagerProjectSummary {
|
||||||
|
id: string;
|
||||||
|
workspaceSlug: string;
|
||||||
|
name: string;
|
||||||
|
identifier: string;
|
||||||
|
memberCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskManagerWorkspaceMembershipResult {
|
export interface TaskManagerWorkspaceMembershipResult {
|
||||||
|
|
@ -52,6 +61,20 @@ export interface TaskManagerWorkspaceMembershipResult {
|
||||||
isBanned: boolean;
|
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 type TaskManagerWorkspaceMemberRole = "unset" | "guest" | "member" | "admin";
|
||||||
|
|
||||||
export interface TaskManagerWorkspaceMembershipMutationResult extends ControlPlaneMutationResult {
|
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> {
|
export async function fetchControlPlaneSnapshot(): Promise<ControlPlaneSnapshot> {
|
||||||
return requestJson<ControlPlaneSnapshot>("/api/admin/control-plane");
|
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> {
|
export async function createAdminGroup(payload: Pick<ClientGroup, "clientId"> & Partial<ClientGroup>): Promise<ControlPlaneMutationResult> {
|
||||||
return requestJson<ControlPlaneMutationResult>("/api/admin/groups", {
|
return requestJson<ControlPlaneMutationResult>("/api/admin/groups", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export interface LauncherData {
|
||||||
syncStatuses: SyncStatus[];
|
syncStatuses: SyncStatus[];
|
||||||
auditEvents: typeof mockAuditEvents;
|
auditEvents: typeof mockAuditEvents;
|
||||||
taskManagerMemberships: TaskManagerMembershipAssignment[];
|
taskManagerMemberships: TaskManagerMembershipAssignment[];
|
||||||
|
taskManagerProjectMemberships: TaskManagerProjectMembershipAssignment[];
|
||||||
settings: LauncherSettings;
|
settings: LauncherSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,6 +77,21 @@ export interface TaskManagerMembershipAssignment {
|
||||||
updatedAt: string;
|
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 {
|
export interface LauncherSettings {
|
||||||
brand: {
|
brand: {
|
||||||
logoLinkUrl: string;
|
logoLinkUrl: string;
|
||||||
|
|
@ -174,6 +190,7 @@ export function normalizeLauncherData(data: Partial<LauncherData> | null | undef
|
||||||
syncStatuses: Array.isArray(payload.syncStatuses) ? payload.syncStatuses : mockSyncStatuses,
|
syncStatuses: Array.isArray(payload.syncStatuses) ? payload.syncStatuses : mockSyncStatuses,
|
||||||
auditEvents: Array.isArray(payload.auditEvents) ? payload.auditEvents : mockAuditEvents,
|
auditEvents: Array.isArray(payload.auditEvents) ? payload.auditEvents : mockAuditEvents,
|
||||||
taskManagerMemberships: Array.isArray(payload.taskManagerMemberships) ? payload.taskManagerMemberships : [],
|
taskManagerMemberships: Array.isArray(payload.taskManagerMemberships) ? payload.taskManagerMemberships : [],
|
||||||
|
taskManagerProjectMemberships: Array.isArray(payload.taskManagerProjectMemberships) ? payload.taskManagerProjectMemberships : [],
|
||||||
settings: normalizeLauncherSettings(payload.settings),
|
settings: normalizeLauncherSettings(payload.settings),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3215,17 +3215,38 @@ code {
|
||||||
|
|
||||||
.task-access-modal {
|
.task-access-modal {
|
||||||
width: min(44rem, calc(100vw - 2rem));
|
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 {
|
.task-access-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 0.65rem;
|
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 {
|
.task-workspace-access-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
min-height: 0;
|
||||||
|
align-content: start;
|
||||||
gap: 0.7rem;
|
gap: 0.7rem;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 0.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-workspace-access-card {
|
.task-workspace-access-card {
|
||||||
|
|
@ -3240,13 +3261,21 @@ code {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 10.8rem;
|
grid-template-columns: minmax(0, 1fr) 10.8rem;
|
||||||
gap: 0.75rem;
|
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 strong,
|
||||||
.task-workspace-access-card__head small {
|
.task-workspace-access-card__head small {
|
||||||
display: block;
|
display: block;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-workspace-access-card__head small {
|
.task-workspace-access-card__head small {
|
||||||
|
|
@ -3255,25 +3284,66 @@ code {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-project-access-note {
|
.task-project-access-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.22rem;
|
gap: 0.55rem;
|
||||||
padding: 0.7rem;
|
padding: 0.7rem;
|
||||||
border-radius: 0.85rem;
|
border-radius: 0.85rem;
|
||||||
background: rgba(0, 0, 0, 0.18);
|
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);
|
color: var(--text-secondary);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-project-access-note span {
|
.task-project-access-list__head span {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
line-height: 1.35;
|
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 {
|
.access-explanation {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ import {
|
||||||
type MeResponse,
|
type MeResponse,
|
||||||
type TaskManagerWorkspaceCreationPolicy,
|
type TaskManagerWorkspaceCreationPolicy,
|
||||||
} from "../../shared/api/mockApi";
|
} 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 { uploadStorageFile } from "../../shared/api/storageApi";
|
||||||
import { cn } from "../../shared/lib/cn";
|
import { cn } from "../../shared/lib/cn";
|
||||||
import { formatDate, formatDateTime } from "../../shared/lib/format";
|
import { formatDate, formatDateTime } from "../../shared/lib/format";
|
||||||
|
|
@ -114,6 +114,14 @@ export interface EnsureTaskManagerWorkspaceMemberCommand {
|
||||||
role: TaskManagerWorkspaceMemberRole;
|
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 }> = [
|
const rootSections: Array<{ id: AdminSection; label: string; icon: React.ReactNode }> = [
|
||||||
{ id: "overview", label: "Обзор", icon: <LayoutDashboard size={16} /> },
|
{ id: "overview", label: "Обзор", icon: <LayoutDashboard size={16} /> },
|
||||||
{ id: "clients", label: "Клиенты", icon: <Building2 size={16} /> },
|
{ id: "clients", label: "Клиенты", icon: <Building2 size={16} /> },
|
||||||
|
|
@ -166,8 +174,10 @@ export function AdminOverlay({
|
||||||
taskManagerWorkspacesLoading,
|
taskManagerWorkspacesLoading,
|
||||||
taskManagerWorkspacesError,
|
taskManagerWorkspacesError,
|
||||||
pendingTaskManagerMemberships,
|
pendingTaskManagerMemberships,
|
||||||
|
pendingTaskManagerProjectMemberships,
|
||||||
onRefreshTaskManagerWorkspaces,
|
onRefreshTaskManagerWorkspaces,
|
||||||
onSetTaskManagerWorkspaceMemberRole,
|
onSetTaskManagerWorkspaceMemberRole,
|
||||||
|
onSetTaskManagerProjectMemberRole,
|
||||||
}: {
|
}: {
|
||||||
data: LauncherData;
|
data: LauncherData;
|
||||||
me: MeResponse;
|
me: MeResponse;
|
||||||
|
|
@ -198,8 +208,10 @@ export function AdminOverlay({
|
||||||
taskManagerWorkspacesLoading: boolean;
|
taskManagerWorkspacesLoading: boolean;
|
||||||
taskManagerWorkspacesError: string | null;
|
taskManagerWorkspacesError: string | null;
|
||||||
pendingTaskManagerMemberships: Record<string, boolean>;
|
pendingTaskManagerMemberships: Record<string, boolean>;
|
||||||
|
pendingTaskManagerProjectMemberships: Record<string, boolean>;
|
||||||
onRefreshTaskManagerWorkspaces: () => void;
|
onRefreshTaskManagerWorkspaces: () => void;
|
||||||
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
|
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
|
||||||
|
onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
|
||||||
}) {
|
}) {
|
||||||
const isRoot = me.launcherRole === "root_admin";
|
const isRoot = me.launcherRole === "root_admin";
|
||||||
const sections = isRoot ? rootSections : clientSections;
|
const sections = isRoot ? rootSections : clientSections;
|
||||||
|
|
@ -375,7 +387,10 @@ export function AdminOverlay({
|
||||||
onUpdateUser={onUpdateUser}
|
onUpdateUser={onUpdateUser}
|
||||||
onUpdateMembership={onUpdateMembership}
|
onUpdateMembership={onUpdateMembership}
|
||||||
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
|
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
|
||||||
|
pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
|
||||||
|
taskManagerWorkspaceCatalog={taskManagerWorkspaces}
|
||||||
onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
|
onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
|
||||||
|
onSetTaskManagerProjectMemberRole={onSetTaskManagerProjectMemberRole}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{activeSection === "invites" ? (
|
{activeSection === "invites" ? (
|
||||||
|
|
@ -948,6 +963,14 @@ const operationalCoreRoleOptions: Array<NodeDcSelectOption<OperationalCoreRoleSe
|
||||||
{ value: "pending", label: "Сохраняем...", disabled: true, hidden: true },
|
{ 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 {
|
function membershipRoleLabel(role: ClientMembershipRole): string {
|
||||||
return membershipRoleOptions.find((option) => option.value === role)?.label ?? role;
|
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";
|
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[] {
|
function normalizeClientTaskManagerWorkspaceDraft(workspaces: ClientTaskManagerWorkspaceBinding[]): ClientTaskManagerWorkspaceBinding[] {
|
||||||
const bySlug = new Map<string, ClientTaskManagerWorkspaceBinding>();
|
const bySlug = new Map<string, ClientTaskManagerWorkspaceBinding>();
|
||||||
|
|
||||||
|
|
@ -2228,7 +2273,10 @@ function AccessSection({
|
||||||
onUpdateUser,
|
onUpdateUser,
|
||||||
onUpdateMembership,
|
onUpdateMembership,
|
||||||
pendingTaskManagerMemberships,
|
pendingTaskManagerMemberships,
|
||||||
|
pendingTaskManagerProjectMemberships,
|
||||||
|
taskManagerWorkspaceCatalog,
|
||||||
onSetTaskManagerWorkspaceMemberRole,
|
onSetTaskManagerWorkspaceMemberRole,
|
||||||
|
onSetTaskManagerProjectMemberRole,
|
||||||
}: {
|
}: {
|
||||||
data: LauncherData;
|
data: LauncherData;
|
||||||
matrix: ReturnType<typeof buildAccessMatrix>;
|
matrix: ReturnType<typeof buildAccessMatrix>;
|
||||||
|
|
@ -2239,10 +2287,13 @@ function AccessSection({
|
||||||
onUpdateUser: (userId: string, patch: Partial<LauncherUser>) => void;
|
onUpdateUser: (userId: string, patch: Partial<LauncherUser>) => void;
|
||||||
onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
|
onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
|
||||||
pendingTaskManagerMemberships: Record<string, boolean>;
|
pendingTaskManagerMemberships: Record<string, boolean>;
|
||||||
|
pendingTaskManagerProjectMemberships: Record<string, boolean>;
|
||||||
|
taskManagerWorkspaceCatalog: TaskManagerWorkspaceSummary[];
|
||||||
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
|
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
|
||||||
|
onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
|
||||||
}) {
|
}) {
|
||||||
const hasUsers = matrix.users.length > 0;
|
const hasUsers = matrix.users.length > 0;
|
||||||
const taskManagerWorkspaces = getClientTaskManagerWorkspaces(matrix.client);
|
const clientTaskManagerWorkspaces = getClientTaskManagerWorkspaces(matrix.client);
|
||||||
const primaryTaskManagerWorkspace = getPrimaryTaskManagerWorkspace(matrix.client);
|
const primaryTaskManagerWorkspace = getPrimaryTaskManagerWorkspace(matrix.client);
|
||||||
const [detailsCell, setDetailsCell] = useState<AccessMatrixCell | null>(null);
|
const [detailsCell, setDetailsCell] = useState<AccessMatrixCell | null>(null);
|
||||||
|
|
||||||
|
|
@ -2399,12 +2450,15 @@ function AccessSection({
|
||||||
user={getUser(data, detailsCell.userId)}
|
user={getUser(data, detailsCell.userId)}
|
||||||
service={getService(data, detailsCell.serviceId)}
|
service={getService(data, detailsCell.serviceId)}
|
||||||
cell={detailsCell}
|
cell={detailsCell}
|
||||||
workspaces={taskManagerWorkspaces}
|
workspaces={clientTaskManagerWorkspaces}
|
||||||
|
workspaceCatalog={taskManagerWorkspaceCatalog}
|
||||||
pendingAccessAssignments={pendingAccessAssignments}
|
pendingAccessAssignments={pendingAccessAssignments}
|
||||||
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
|
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
|
||||||
|
pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
|
||||||
onClose={() => setDetailsCell(null)}
|
onClose={() => setDetailsCell(null)}
|
||||||
onSetUserServiceAccess={onSetUserServiceAccess}
|
onSetUserServiceAccess={onSetUserServiceAccess}
|
||||||
onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
|
onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
|
||||||
|
onSetTaskManagerProjectMemberRole={onSetTaskManagerProjectMemberRole}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2501,11 +2555,14 @@ function OperationalCoreAccessModal({
|
||||||
service,
|
service,
|
||||||
cell,
|
cell,
|
||||||
workspaces,
|
workspaces,
|
||||||
|
workspaceCatalog,
|
||||||
pendingAccessAssignments,
|
pendingAccessAssignments,
|
||||||
pendingTaskManagerMemberships,
|
pendingTaskManagerMemberships,
|
||||||
|
pendingTaskManagerProjectMemberships,
|
||||||
onClose,
|
onClose,
|
||||||
onSetUserServiceAccess,
|
onSetUserServiceAccess,
|
||||||
onSetTaskManagerWorkspaceMemberRole,
|
onSetTaskManagerWorkspaceMemberRole,
|
||||||
|
onSetTaskManagerProjectMemberRole,
|
||||||
}: {
|
}: {
|
||||||
data: LauncherData;
|
data: LauncherData;
|
||||||
client: Client;
|
client: Client;
|
||||||
|
|
@ -2513,11 +2570,14 @@ function OperationalCoreAccessModal({
|
||||||
service: Service;
|
service: Service;
|
||||||
cell: AccessMatrixCell;
|
cell: AccessMatrixCell;
|
||||||
workspaces: ClientTaskManagerWorkspaceBinding[];
|
workspaces: ClientTaskManagerWorkspaceBinding[];
|
||||||
|
workspaceCatalog: TaskManagerWorkspaceSummary[];
|
||||||
pendingAccessAssignments: Record<string, AccessAssignmentValue>;
|
pendingAccessAssignments: Record<string, AccessAssignmentValue>;
|
||||||
pendingTaskManagerMemberships: Record<string, boolean>;
|
pendingTaskManagerMemberships: Record<string, boolean>;
|
||||||
|
pendingTaskManagerProjectMemberships: Record<string, boolean>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
|
onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
|
||||||
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
|
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
|
||||||
|
onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
|
||||||
}) {
|
}) {
|
||||||
const membership = data.memberships.find((item) => item.clientId === client.id && item.userId === user.id);
|
const membership = data.memberships.find((item) => item.clientId === client.id && item.userId === user.id);
|
||||||
const protectedUser = user.id === "user_root" || membership?.role === "client_owner";
|
const protectedUser = user.id === "user_root" || membership?.role === "client_owner";
|
||||||
|
|
@ -2538,6 +2598,7 @@ function OperationalCoreAccessModal({
|
||||||
{workspaces.length ? (
|
{workspaces.length ? (
|
||||||
workspaces.map((workspace) => {
|
workspaces.map((workspace) => {
|
||||||
const role = getTaskManagerMembershipRole(data, client.id, user.id, workspace.slug);
|
const role = getTaskManagerMembershipRole(data, client.id, user.id, workspace.slug);
|
||||||
|
const projects = getWorkspaceCatalogProjects(workspace, workspaceCatalog);
|
||||||
const pendingKey = `${client.id}:${user.id}:${workspace.slug}`;
|
const pendingKey = `${client.id}:${user.id}:${workspace.slug}`;
|
||||||
const pending = Boolean(pendingTaskManagerMemberships[pendingKey]) || basePendingValue !== undefined;
|
const pending = Boolean(pendingTaskManagerMemberships[pendingKey]) || basePendingValue !== undefined;
|
||||||
const value: OperationalCoreRoleSelectValue = pending
|
const value: OperationalCoreRoleSelectValue = pending
|
||||||
|
|
@ -2580,9 +2641,68 @@ function OperationalCoreAccessModal({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="task-project-access-note">
|
<div className="task-project-access-list">
|
||||||
<strong>Проекты</strong>
|
<div className="task-project-access-list__head">
|
||||||
<span>Сейчас наследуют роль workspace. Точечные роли проектов подключаются следующим Project API-адаптером.</span>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue