@@ -41,7 +41,7 @@ import {
X ,
} from "lucide-react" ;
import type { ServiceAppRole } from "../../entities/access/types" ;
import type { Client , ClientStatus , ClientType } from "../../entities/client/types" ;
import type { Client , ClientStatus , ClientTaskManagerWorkspaceBinding , ClientType } from "../../entities/client/types" ;
import type { Invite , InviteStatus } from "../../entities/invite/types" ;
import { createServiceLaunchLinkPatch , getServiceLaunchLink } from "../../entities/service/links" ;
import type { MediaKind , Service , ServiceMediaSource , ServiceStatus } from "../../entities/service/types" ;
@@ -89,7 +89,7 @@ type AdminSection =
type AccessAssignmentRole = Exclude < ServiceAppRole , "owner" > ;
export type AccessAssignmentValue = AccessAssignmentRole | "deny" | "unset" ;
type TaskManagerRoleSelectValue = TaskManagerWorkspaceMemberRole | "pending" ;
type OperationalCoreRoleSelectValue = AccessAssignmentValue | "pending" ;
export interface SetUserServiceAccessCommand {
userId : string ;
@@ -110,6 +110,7 @@ export interface CreateUserCommand {
export interface EnsureTaskManagerWorkspaceMemberCommand {
clientId : string ;
userId : string ;
workspaceSlug? : string ;
role : TaskManagerWorkspaceMemberRole ;
}
@@ -936,27 +937,16 @@ const auditResultOptions: Array<AdminStatusOption<"success" | "warning" | "error
const accessAssignmentOptions : Array < NodeDcSelectOption < AccessAssignmentValue > > = [
{ value : "unset" , label : "—" , description : "Не назначен" } ,
{ value : "viewer" , label : "viewer " , description : "Просмотр" , tone : "green" } ,
{ value : "member" , label : "member " , description : "Участник " , tone : "green" } ,
{ value : "admin" , label : "admin " , description : "Администратор " , tone : "green" } ,
{ value : "deny" , label : "Deny " , description : "Исключение " , tone : "red" } ,
{ value : "viewer" , label : "Гость " , description : "Просмотр" , tone : "green" } ,
{ value : "member" , label : "Участник " , description : "Рабочий доступ " , tone : "green" } ,
{ value : "admin" , label : "Админ " , description : "Администрирование " , tone : "green" } ,
{ value : "deny" , label : "Заблокирован " , description : "Запрет доступа " , tone : "red" } ,
] ;
function buildTaskManagerRoleOptions ( {
hasWorkspace ,
disabled ,
} : {
hasWorkspace : boolean ;
disabled : boolean ;
} ) : Array < NodeDcSelectOption < TaskManagerRoleSelectValue > > {
return [
{ value : "unset" , label : hasWorkspace ? "—" : "Workspace не выбран" } ,
{ value : "guest" , label : "Гость" , disabled : ! hasWorkspace || disabled } ,
{ value : "member" , label : "Участник" , disabled : ! hasWorkspace || disabled } ,
{ value : "admin" , label : "Админ" , disabled : ! hasWorkspace || disabled } ,
{ value : "pending" , label : "Сохраняем..." , disabled : true , hidden : true } ,
] ;
}
const operationalCoreRoleOptions : Array < NodeDcSelectOption < OperationalCoreRoleSelectValue > > = [
. . . accessAssignmentOptions ,
{ value : "pending" , label : "Сохраняем..." , disabled : true , hidden : true } ,
] ;
function membershipRoleLabel ( role : ClientMembershipRole ) : string {
return membershipRoleOptions . find ( ( option ) = > option . value === role ) ? . label ? ? role ;
@@ -970,16 +960,76 @@ function mainStatusLabel(value: LauncherUserStatus): string {
return mainStatusOptions . find ( ( option ) = > option . value === value ) ? . label ? ? value ;
}
function taskManagerRoleLabel ( role : TaskManagerWorkspaceMemberRole | TaskManagerRoleSelectValue ) : string {
const labels : Record < TaskManagerRoleSelectValue , string > = {
unset : "—" ,
guest : "Гость" ,
member : "Участник" ,
admin : "Админ" ,
pending : "Сохраняем..." ,
} ;
function getClientTaskManagerWorkspaces ( client : Client ) : ClientTaskManagerWorkspaceBinding [ ] {
const taskManager = client . integrations ? . taskManager ;
const bySlug = new Map < string , ClientTaskManagerWorkspaceBinding > ( ) ;
return labels [ role ] ;
for ( const workspace of taskManager ? . workspaces ? ? [ ] ) {
if ( ! workspace . slug ) continue ;
bySlug . set ( workspace . slug , {
slug : workspace.slug ,
name : workspace.name ? ? null ,
isPrimary : workspace.isPrimary === true ,
} ) ;
}
if ( taskManager ? . workspaceSlug && ! bySlug . has ( taskManager . workspaceSlug ) ) {
bySlug . set ( taskManager . workspaceSlug , {
slug : taskManager.workspaceSlug ,
name : taskManager.workspaceName ? ? null ,
isPrimary : true ,
} ) ;
}
const workspaces = [ . . . bySlug . values ( ) ] ;
if ( ! workspaces . length ) return [ ] ;
if ( ! workspaces . some ( ( workspace ) = > workspace . isPrimary ) ) {
workspaces [ 0 ] . isPrimary = true ;
}
let primarySeen = false ;
return workspaces . map ( ( workspace ) = > {
const isPrimary = workspace . isPrimary === true && ! primarySeen ;
if ( isPrimary ) primarySeen = true ;
return { . . . workspace , isPrimary } ;
} ) ;
}
function getPrimaryTaskManagerWorkspace ( client : Client ) : ClientTaskManagerWorkspaceBinding | null {
const workspaces = getClientTaskManagerWorkspaces ( client ) ;
return workspaces . find ( ( workspace ) = > workspace . isPrimary ) ? ? workspaces [ 0 ] ? ? null ;
}
function getTaskManagerMembershipRole ( data : LauncherData , clientId : string , userId : string , workspaceSlug : string ) : TaskManagerWorkspaceMemberRole {
return data . taskManagerMemberships . find ( ( membership ) = > membership . clientId === clientId && membership . userId === userId && membership . workspaceSlug === workspaceSlug ) ? . role ? ? "unset" ;
}
function normalizeClientTaskManagerWorkspaceDraft ( workspaces : ClientTaskManagerWorkspaceBinding [ ] ) : ClientTaskManagerWorkspaceBinding [ ] {
const bySlug = new Map < string , ClientTaskManagerWorkspaceBinding > ( ) ;
for ( const workspace of workspaces ) {
if ( ! workspace . slug ) continue ;
bySlug . set ( workspace . slug , {
slug : workspace.slug ,
name : workspace.name ? ? null ,
isPrimary : workspace.isPrimary === true ,
} ) ;
}
const normalized = [ . . . bySlug . values ( ) ] ;
if ( ! normalized . length ) return [ ] ;
if ( ! normalized . some ( ( workspace ) = > workspace . isPrimary ) ) {
normalized [ 0 ] . isPrimary = true ;
}
let primarySeen = false ;
return normalized . map ( ( workspace ) = > {
const isPrimary = workspace . isPrimary === true && ! primarySeen ;
if ( isPrimary ) primarySeen = true ;
return { . . . workspace , isPrimary } ;
} ) ;
}
function AdminStaticPill ( { children } : { children : ReactNode } ) {
@@ -1609,15 +1659,9 @@ function ClientEditorModal({
canDelete : boolean ;
} ) {
const [ draft , setDraft ] = useState < Client > ( client ) ;
const taskManagerWorkspaceOptions : Array < NodeDcSelectOption < string > > = [
{ value : "none" , label : "Не привязан" } ,
. . . taskManagerWorkspaces . map ( ( workspace ) = > ( {
value : workspace.slug ,
label : workspace.name ,
description : ` ${ workspace . slug } · ${ workspace . memberCount } участников ` ,
} ) ) ,
] ;
const selectedTaskManagerWorkspaceSlug = draft . integrations ? . taskManager ? . workspaceSlug ? ? "none" ;
const taskManagerWorkspaceBindings = getClientTaskManagerWorkspaces ( draft ) ;
const selectedTaskManagerWorkspaceSlugs = new Set ( taskManagerWorkspaceBindings . map ( ( workspace ) = > workspace . slug ) ) ;
const primaryTaskManagerWorkspace = getPrimaryTaskManagerWorkspace ( draft ) ;
useEffect ( ( ) = > setDraft ( client ) , [ client ] ) ;
@@ -1625,26 +1669,66 @@ function ClientEditorModal({
setDraft ( ( current ) = > ( { . . . current , [ key ] : value } ) ) ;
}
function updateTaskManagerWorkspace ( workspaceSlug : string ) {
const selectedWorkspace = taskManagerWorkspaces . find ( ( workspace ) = > workspace . slug === workspaceSlug ) ;
function updateTaskManagerWorkspaces ( workspaces : ClientTaskManagerWorkspaceBinding [ ] ) {
const primaryWorkspace = workspaces . find ( ( workspace ) = > workspace . isPrimary ) ? ? workspaces [ 0 ] ? ? null ;
setDraft ( ( current ) = > ( {
. . . current ,
integrations : {
. . . current . integrations ,
taskManager : {
. . . current . integrations ? . taskManager ,
workspaceSlug : workspaceSlug === "none" ? null : workspaceSlug ,
workspaceName : selected Workspace?.name ? ? null ,
workspaceSlug : primaryWorkspace?.slug ? ? null ,
workspaceName : primary Workspace?.name ? ? null ,
workspaces ,
} ,
} ,
} ) ) ;
}
function toggleTaskManagerWorkspace ( workspace : TaskManagerWorkspaceSummary ) {
const currentWorkspaces = getClientTaskManagerWorkspaces ( draft ) ;
const exists = currentWorkspaces . some ( ( item ) = > item . slug === workspace . slug ) ;
const nextWorkspaces = exists
? currentWorkspaces . filter ( ( item ) = > item . slug !== workspace . slug )
: [
. . . currentWorkspaces ,
{
slug : workspace.slug ,
name : workspace.name ,
isPrimary : currentWorkspaces.length === 0 ,
} ,
] ;
updateTaskManagerWorkspaces ( normalizeClientTaskManagerWorkspaceDraft ( nextWorkspaces ) ) ;
}
function setPrimaryTaskManagerWorkspace ( workspaceSlug : string ) {
const nextWorkspaces = getClientTaskManagerWorkspaces ( draft ) . map ( ( workspace ) = > ( {
. . . workspace ,
isPrimary : workspace.slug === workspaceSlug ,
} ) ) ;
updateTaskManagerWorkspaces ( normalizeClientTaskManagerWorkspaceDraft ( nextWorkspaces ) ) ;
}
return (
< div className = "service-content-modal-layer" role = "dialog" aria-modal = "true" aria-label = { ` Редактор клиента ${ client . name } ` } >
< article className = "service-content-modal admin-entity-modal" >
< EntityModalHead eyebrow = "Клиент" title = { client . name } onClose = { onClose } / >
< EntityModalHead
eyebrow = "Клиент"
title = { client . name }
onClose = { onClose }
actions = {
< IconButton
label = { taskManagerWorkspacesLoading ? "Обновляем workspace Operational Core" : "Обновить workspace Operational Core" }
className = "admin-circle-action admin-circle-action--solid"
type = "button"
disabled = { taskManagerWorkspacesLoading }
onClick = { onRefreshTaskManagerWorkspaces }
>
< RefreshCw size = { 16 } / >
< / IconButton >
}
/ >
< div className = "service-content-modal__grid" >
< label className = "service-content-field" >
< span > Н а з в а н и е < / span >
@@ -1674,32 +1758,58 @@ function ClientEditorModal({
< AdminStatusDropdown value = { draft . status } options = { clientStatusOptions } label = "Статус клиента" onChange = { ( status ) = > update ( "status" , status ) } / >
< / div >
< div className = "service-content-field service-content-field--wide" >
< span > Operational Core workspace < / span >
< div className = "admin-field-row " >
< NodeDcSelect
className = "admin-modal-select-wrap"
triggerClassName = "admin-modal-select-trigger"
value = { selectedTaskManagerWorkspaceSlug }
options = { taskManagerWorkspaceOptions }
label = "Operational Core workspace"
searchable
minMenuWidth = { 280 }
onChange = { updateTaskManagerWorkspace }
/ >
< IconButton
label = { taskManagerWorkspacesLoading ? "Обновляем workspace Operational Core" : "Обновить workspace Operational Core" }
className = "admin-circle-action admin-circle-action--solid"
type = "button"
disabled = { taskManagerWorkspacesLoading }
onClick = { onRefreshTaskManagerWorkspaces }
>
< RefreshCw size = { 16 } / >
< / IconButton >
< span > Operational Core workspaces < / span >
< div className = "task-workspace-picker-card " >
< div className = "task-workspace-picker" role = "list" aria-label = "Operational Core workspaces клиента" >
{ taskManagerWorkspaces . length ? (
taskManagerWorkspaces . map ( ( workspace ) = > {
const selected = selectedTaskManagerWorkspaceSlugs . has ( workspace . slug ) ;
const primary = primaryTaskManagerWorkspace ? . slug === workspace . slug ;
return (
< button
key = { workspace . slug }
className = { cn ( "task-workspace-chip" , selected && "task-workspace-chip--selected" ) }
type = "button"
aria-pressed = { selected }
onClick = { ( ) = > toggleTaskManagerWorkspace ( workspace ) }
>
< span >
< strong > { workspace . name } < / strong >
< small >
{ workspace . slug } · { workspace . memberCount } у ч а с т н и к о в
< / small >
< / span >
{ selected ? (
< em
role = "button"
tabIndex = { 0 }
onClick = { ( event ) = > {
event . stopPropagation ( ) ;
setPrimaryTaskManagerWorkspace ( workspace . slug ) ;
} }
onKeyDown = { ( event ) = > {
if ( event . key !== "Enter" && event . key !== " " ) return ;
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
setPrimaryTaskManagerWorkspace ( workspace . slug ) ;
} }
>
{ primary ? "Основной" : "Сделать основным" }
< / em >
) : null }
< / button >
) ;
} )
) : (
< span className = "task-workspace-picker__empty" > Workspace Operational Core н е з а г р у ж е н ы < / span >
) }
< / div >
< / div >
< small >
{ taskManagerWorkspacesError
? taskManagerWorkspacesError
: "Эта привязка используется для назначения участников клиента в workspace Task Manager ." }
: "Эти workspace доступны для детальных назначений пользователей в Operational Core ." }
< / small >
< / div >
< label className = "service-content-field" >
@@ -1945,16 +2055,29 @@ function GroupEditorModal({
) ;
}
function EntityModalHead ( { eyebrow , title , onClose } : { eyebrow : string ; title : string ; onClose : ( ) = > void } ) {
function EntityModalHead ( {
eyebrow ,
title ,
onClose ,
actions ,
} : {
eyebrow : string ;
title : string ;
onClose : ( ) = > void ;
actions? : ReactNode ;
} ) {
return (
< div className = "service-content-modal__head" >
< div >
< p className = "eyebrow" > { eyebrow } < / p >
< h3 > { title } < / h3 >
< / div >
< IconButton label = "Закрыть редактор" className = "admin-circle-action" type = "button" onClick = { onClose } >
< X size = { 15 } strokeWidth = { 1.45 } / >
< / IconButton >
< div className = "service-content-modal__head-actions" >
{ actions }
< IconButton label = "Закрыть редактор" className = "admin-circle-action" type = "button" onClick = { onClose } >
< X size = { 15 } strokeWidth = { 1.45 } / >
< / IconButton >
< / div >
< / div >
) ;
}
@@ -2119,7 +2242,9 @@ function AccessSection({
onSetTaskManagerWorkspaceMemberRole : ( command : EnsureTaskManagerWorkspaceMemberCommand ) = > void ;
} ) {
const hasUsers = matrix . users . length > 0 ;
const taskManagerWorkspace = matrix . client . integrations ? . taskManager ? . workspaceSlug ? ? null ;
const taskManagerWorkspaces = getClientTaskManagerWorkspaces ( matrix . client ) ;
const primaryTaskManagerWorkspace = getPrimaryTaskManagerWorkspace ( matrix . client ) ;
const [ detailsCell , setDetailsCell ] = useState < AccessMatrixCell | null > ( null ) ;
if ( ! hasUsers ) {
return (
@@ -2149,7 +2274,7 @@ function AccessSection({
const selectedUser = selectedCell ? getUser ( data , selectedCell . userId ) : null ;
const selectedService = selectedCell ? getService ( data , selectedCell . serviceId ) : null ;
const accessGridTemplateColumns = ` 15rem repeat( ${ matrix . services . length + 2 } , 9.7 rem) ` ;
const accessGridTemplateColumns = ` 15rem repeat(2, 9.7rem) repeat( ${ matrix . services . length } , 11.25 rem) ` ;
return (
< div className = "access-layout" >
@@ -2180,7 +2305,7 @@ function AccessSection({
if ( ! membership ) return null ;
const protectedUser = user . id === "user_root" ;
const pendingKey = ` ${ matrix . client . id } : ${ user . id } ` ;
const pendingKey = ` ${ matrix . client . id } : ${ user . id } : ${ primaryTaskManagerWorkspace ? . slug ? ? "primary" } ` ;
const pendingTaskerAssignment = Boolean ( pendingTaskManagerMemberships [ pendingKey ] ) ;
const forcedTaskManagerAdmin = membership . role === "client_owner" ;
@@ -2220,14 +2345,16 @@ function AccessSection({
onSetAccess = { ( value ) = > {
onSetUserServiceAccess ( { userId : user.id , serviceId : service.id , value } ) ;
if ( ! isTaskManagerService || ! taskManagerWorkspace || protectedUser || forcedTaskManagerAdmin ) return ;
if ( ! isTaskManagerService || ! primaryTaskManagerWorkspace || protectedUser || forcedTaskManagerAdmin ) return ;
onSetTaskManagerWorkspaceMemberRole ( {
clientId : matrix.client.id ,
userId : user.id ,
workspaceSlug : primaryTaskManagerWorkspace.slug ,
role : accessAssignmentToTaskManagerRole ( value ) ,
} ) ;
} }
onOpenDetails = { isTaskManagerService ? ( ) = > setDetailsCell ( cell ) : undefined }
/ >
< / div >
) ;
@@ -2264,6 +2391,22 @@ function AccessSection({
< / >
) }
< / GlassSurface >
{ detailsCell ? (
< OperationalCoreAccessModal
data = { data }
client = { matrix . client }
user = { getUser ( data , detailsCell . userId ) }
service = { getService ( data , detailsCell . serviceId ) }
cell = { detailsCell }
workspaces = { taskManagerWorkspaces }
pendingAccessAssignments = { pendingAccessAssignments }
pendingTaskManagerMemberships = { pendingTaskManagerMemberships }
onClose = { ( ) = > setDetailsCell ( null ) }
onSetUserServiceAccess = { onSetUserServiceAccess }
onSetTaskManagerWorkspaceMemberRole = { onSetTaskManagerWorkspaceMemberRole }
/ >
) : null }
< / div >
) ;
}
@@ -2351,6 +2494,117 @@ function MainRoleControl({
) ;
}
function OperationalCoreAccessModal ( {
data ,
client ,
user ,
service ,
cell ,
workspaces ,
pendingAccessAssignments ,
pendingTaskManagerMemberships ,
onClose ,
onSetUserServiceAccess ,
onSetTaskManagerWorkspaceMemberRole ,
} : {
data : LauncherData ;
client : Client ;
user : LauncherUser ;
service : Service ;
cell : AccessMatrixCell ;
workspaces : ClientTaskManagerWorkspaceBinding [ ] ;
pendingAccessAssignments : Record < string , AccessAssignmentValue > ;
pendingTaskManagerMemberships : Record < string , boolean > ;
onClose : ( ) = > void ;
onSetUserServiceAccess : ( command : SetUserServiceAccessCommand ) = > void ;
onSetTaskManagerWorkspaceMemberRole : ( command : EnsureTaskManagerWorkspaceMemberCommand ) = > 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" ;
const basePendingValue = pendingAccessAssignments [ accessCellKey ( user . id , service . id ) ] ;
const baseAssignmentValue = basePendingValue ? ? accessAssignmentValue ( cell ) ;
return (
< div className = "service-content-modal-layer" role = "dialog" aria-modal = "true" aria-label = { ` Operational Core доступы ${ user . name } ` } >
< article className = "service-content-modal admin-entity-modal task-access-modal" >
< EntityModalHead eyebrow = "Доступы → Operational Core" title = { user . name } onClose = { onClose } / >
< div className = "task-access-summary" >
< InfoLine label = "Клиент" value = { client . name } / >
< InfoLine label = "Сервис" value = { service . title } / >
< InfoLine label = "Пользователь" value = { user . email } / >
< / div >
< div className = "task-workspace-access-list" >
{ workspaces . length ? (
workspaces . map ( ( workspace ) = > {
const role = getTaskManagerMembershipRole ( data , client . id , user . id , workspace . slug ) ;
const pendingKey = ` ${ client . id } : ${ user . id } : ${ workspace . slug } ` ;
const pending = Boolean ( pendingTaskManagerMemberships [ pendingKey ] ) || basePendingValue !== undefined ;
const value : OperationalCoreRoleSelectValue = pending
? "pending"
: baseAssignmentValue === "deny"
? "deny"
: taskManagerRoleToAccessAssignment ( role ) ;
return (
< section key = { workspace . slug } className = "task-workspace-access-card" >
< div className = "task-workspace-access-card__head" >
< div >
< strong > { workspace . name ? ? workspace . slug } < / strong >
< small >
{ workspace . slug }
{ workspace . isPrimary ? " · основной workspace" : "" }
< / small >
< / div >
{ protectedUser ? (
< AdminStaticPill > { accessAssignmentLabel ( "admin" ) } < / AdminStaticPill >
) : (
< NodeDcSelect
className = "admin-table-select-wrap"
triggerClassName = "admin-table-select-trigger"
value = { value }
options = { operationalCoreRoleOptions }
label = { ` Роль ${ user . name } в ${ workspace . name ? ? workspace . slug } ` }
minMenuWidth = { 180 }
disabled = { pending }
onChange = { ( nextValue ) = > {
if ( nextValue === "pending" ) return ;
onSetUserServiceAccess ( { userId : user.id , serviceId : service.id , value : nextValue } ) ;
onSetTaskManagerWorkspaceMemberRole ( {
clientId : client.id ,
userId : user.id ,
workspaceSlug : workspace.slug ,
role : accessAssignmentToTaskManagerRole ( nextValue ) ,
} ) ;
} }
/ >
) }
< / div >
< div className = "task-project-access-note" >
< strong > П р о е к т ы < / strong >
< span > С е й ч а с н а с л е д у ю т р о л ь workspace . Т о ч е ч н ы е р о л и п р о е к т о в п о д к л ю ч а ю т с я с л е д у ю щ и м Project API - а д а п т е р о м . < / span >
< / div >
< / section >
) ;
} )
) : (
< div className = "access-empty-state" >
< strong > Workspace н е п р и в я з а н ы к к л и е н т у < / strong >
< span > О т к р о й т е к а р т о ч к у к л и е н т а и в ы б е р и т е Operational Core workspaces , п о с л е э т о г о з д е с ь п о я в я т с я н а з н а ч е н и я . < / span >
< / div >
) }
< / div >
< div className = "service-content-modal__foot" >
< Button variant = "secondary" surface = "modal" type = "button" onClick = { onClose } >
З а к р ы т ь
< / Button >
< / div >
< / article >
< / div >
) ;
}
function AccessCellControl ( {
cell ,
active ,
@@ -2358,6 +2612,7 @@ function AccessCellControl({
busy = false ,
onSelectCell ,
onSetAccess ,
onOpenDetails ,
} : {
cell : AccessMatrixCell ;
active : boolean ;
@@ -2365,9 +2620,37 @@ function AccessCellControl({
busy? : boolean ;
onSelectCell : ( cell : AccessMatrixCell ) = > void ;
onSetAccess : ( value : AccessAssignmentValue ) = > void ;
onOpenDetails ? : ( ) = > void ;
} ) {
const isPending = pendingValue !== undefined || busy ;
const assignmentValue = pendingValue ? ? accessAssignmentValue ( cell ) ;
const cellClassName = cn (
"access-cell" ,
onOpenDetails && "access-cell--modal" ,
cell . effectiveAccess . allowed && "access-cell--allowed" ,
! cell . effectiveAccess . allowed && "access-cell--denied" ,
cell . effectiveAccess . source === "exception" && "access-cell--exception" ,
isPending && "access-cell--pending" ,
active && "access-cell--active"
) ;
if ( onOpenDetails ) {
return (
< button
className = { cellClassName }
type = "button"
aria-busy = { isPending }
disabled = { isPending }
onClick = { ( ) = > {
onSelectCell ( cell ) ;
onOpenDetails ( ) ;
} }
>
< strong > { isPending ? accessAssignmentLabel ( assignmentValue ) : accessCellTitle ( cell ) } < / strong >
< span > { isPending ? "Сохраняем..." : sourceLabel ( cell . effectiveAccess . source ) } < / span >
< / button >
) ;
}
return (
< NodeDcSelect
@@ -2381,14 +2664,7 @@ function AccessCellControl({
trigger = { ( { open , toggle , setTriggerRef } ) = > (
< button
ref = { setTriggerRef }
className = { cn (
"access-cell" ,
cell . effectiveAccess . allowed && "access-cell--allowed" ,
! cell . effectiveAccess . allowed && "access-cell--denied" ,
cell . effectiveAccess . source === "exception" && "access-cell--exception" ,
isPending && "access-cell--pending" ,
active && "access-cell--active"
) }
className = { cellClassName }
type = "button"
aria-expanded = { open }
aria-busy = { isPending }
@@ -2867,8 +3143,8 @@ function sectionTitle(section: AdminSection): string {
}
function accessCellTitle ( cell : AccessMatrixCell ) : string {
if ( ! cell . effectiveAccess . allowed ) return cell . effectiveAccess . source === "exception" ? "D eny" : "—" ;
return cell . effectiveAccess . appRole ? ? "allow" ;
if ( ! cell . effectiveAccess . allowed ) return cell . effectiveAccess . source === "exception" ? accessAssignmentLabel ( "d eny" ) : "—" ;
return accessAssignmentRoleLabel ( cell . effectiveAccess . appRole ) ;
}
function accessAssignmentValue ( cell : AccessMatrixCell ) : AccessAssignmentValue {
@@ -2883,6 +3159,11 @@ function accessAssignmentLabel(value: AccessAssignmentValue): string {
return accessAssignmentOptions . find ( ( option ) = > option . value === value ) ? . label ? ? value ;
}
function accessAssignmentRoleLabel ( role? : ServiceAppRole | null ) : string {
if ( ! role ) return "—" ;
return accessAssignmentLabel ( role === "owner" ? "admin" : role ) ;
}
function accessCellKey ( userId : string , serviceId : string ) : string {
return ` ${ userId } : ${ serviceId } ` ;
}
@@ -2897,6 +3178,12 @@ function accessAssignmentToTaskManagerRole(value: AccessAssignmentValue): TaskMa
return "unset" ;
}
function taskManagerRoleToAccessAssignment ( role : TaskManagerWorkspaceMemberRole ) : AccessAssignmentValue {
if ( role === "admin" || role === "member" ) return role ;
if ( role === "guest" ) return "viewer" ;
return "unset" ;
}
function sourceLabel ( source? : AccessMatrixCell [ "effectiveAccess" ] [ "source" ] ) : string {
if ( ! source ) return "—" ;
const labels = {