sozsoft-platform/ui/src/views/admin/role-management/RolesPermission.tsx
2026-06-26 01:55:44 +03:00

608 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import classNames from 'classnames'
import { FaChevronRight, FaChevronDown } from 'react-icons/fa'
import Container from '@/components/shared/Container'
import { Button, Checkbox, Dialog, Input, Menu, toast } from '@/components/ui'
import { useConfig } from '@/components/ui/ConfigProvider'
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
import Notification from '@/components/ui/Notification'
import {
GetPermissionListResultDto,
PermissionGrantInfoDto,
PermissionGroupDto,
PermissionWithGroupName,
PermissionWithStyle,
} from '@/proxy/admin/models'
import { getPermissions, getRoles, updatePermissions } from '@/services/identity.service'
import { useStoreActions, useStoreState } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { ChangeEvent, useEffect, useMemo, useState } from 'react'
type OpenState = Record<string, boolean>
function PermissionDialogContent({
name,
translate,
permissionList,
selectedGroup,
selectedGroupPermissions,
filteredPermissions,
isAllSelected,
isAllSelectedForGroup,
isLoading,
searchTerm,
openPermissions,
mode,
onSelectAll,
onDialogClose,
onDialogOk,
handleExpandAll,
handleCollapseAll,
changeGroup,
togglePermission,
isParent,
getChildren,
onClickCheckbox,
setSearchTerm,
setCopyDialogOpen,
}: any) {
const { isMaximized } = useDialogContext()
return (
<>
<h5 className="mb-1 flex-shrink-0">
{translate('::Permission')} - {name}
</h5>
<hr className="mt-1 mb-2 flex-shrink-0"></hr>
<div className="flex flex-col md:flex-row gap-4 mb-1 flex-shrink-0">
<div className="w-full md:w-1/3">
<Checkbox name="all" checked={isAllSelected} onChange={onSelectAll}>
{translate('AbpPermissionManagement::SelectAllInAllTabs')}
</Checkbox>
</div>
<div className="w-full md:w-2/3">
<Checkbox name="group" checked={isAllSelectedForGroup} onChange={onSelectAll}>
{translate('AbpPermissionManagement::SelectAllInThisTab')}
</Checkbox>
<Button size="sm" variant="plain" onClick={handleExpandAll} icon={<FaChevronRight />}>
{translate('::ListForms.ListFormEdit.ExpandAll')}
</Button>
<Button size="sm" variant="plain" onClick={handleCollapseAll} icon={<FaChevronDown />}>
{translate('::ListForms.ListFormEdit.CollapseAll')}
</Button>
</div>
</div>
<div
className={classNames(
'flex flex-col md:flex-row gap-4',
isMaximized ? 'flex-1 min-h-0' : '',
)}
>
<div
className={classNames(
'w-full md:w-1/3 overflow-y-auto',
isMaximized ? 'min-h-0' : 'max-h-[450px]',
)}
>
<hr className="mb-2"></hr>
<Menu
className="w-full"
variant={mode}
defaultActiveKeys={[selectedGroup?.displayName ?? '']}
>
{permissionList?.groups.map((group: any) => (
<Menu.MenuItem
key={group.name}
className="break-all whitespace-normal"
eventKey={group.name}
onSelect={changeGroup}
>
{translate('::' + group.displayName)} (
{group.permissions.filter((a: any) => a.isGranted).length})
</Menu.MenuItem>
))}
</Menu>
</div>
<div
className={classNames(
'w-full md:w-2/3 overflow-y-auto flex flex-col',
isMaximized ? 'min-h-0' : 'max-h-[450px]',
)}
>
<hr className="mb-2 flex-shrink-0"></hr>
<div className="flex items-center gap-2 mb-2 flex-shrink-0">
<Input
size="sm"
placeholder={translate('::Search')}
value={searchTerm}
onChange={(e: any) => setSearchTerm(e.target.value)}
/>
</div>
<div className="my-1">
{filteredPermissions.map((permission: any) => {
const isParentPerm = isParent(permission)
const permKey = permission.name || ''
return (
<div key={permission.name} className={`ml-${permission.level * 4} group`}>
<div className="flex items-center gap-2 px-2 py-0.5 rounded-md hover:bg-gray-50 transition-all dark:hover:bg-gray-700">
{isParentPerm ? (
<Button
type="button"
onClick={(e) => {
e.stopPropagation()
togglePermission(permKey)
}}
variant="default"
shape="none"
className="!h-5 !w-5 !rounded !border-transparent !bg-transparent !p-0 transition hover:!bg-transparent active:!bg-transparent dark:hover:!bg-transparent dark:active:!bg-transparent"
>
{openPermissions[permKey] || searchTerm ? (
<FaChevronDown className="text-gray-500 text-xs" />
) : (
<FaChevronRight className="text-gray-500 text-xs" />
)}
</Button>
) : (
<div className="w-5" />
)}
<Checkbox
name={permission.name}
checked={permission.isGranted}
onChange={() => onClickCheckbox(permission)}
>
<span className="text-sm text-gray-700 group-hover:text-gray-900 transition dark:text-gray-300 dark:group-hover:text-gray-100">
{translate('::' + permission.displayName)}
</span>
</Checkbox>
</div>
</div>
)
})}
</div>
</div>
</div>
<Dialog.Footer className="flex justify-between items-center mt-4 pt-3 border-t">
<div className="flex items-center">
<Button variant="solid" color="sky-500" onClick={() => setCopyDialogOpen(true)}>
{translate('::AbpIdentity.Roles.CopyPermissions')}
</Button>
</div>
<div className="text-right">
<Button className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={onDialogClose}>
{translate('::Cancel')}
</Button>
<Button variant="solid" loading={isLoading} onClick={onDialogOk}>
{isLoading ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</div>
</Dialog.Footer>
</>
)
}
function RolesPermission({
open,
onDialogClose,
name,
}: {
open: boolean
onDialogClose: () => void
name: string
}) {
// Collapse/expand state
const [openPermissions, setOpenPermissions] = useState<OpenState>({})
// Parent izinleri aç/kapat
const togglePermission = (permName: string) => {
setOpenPermissions((prev) => ({ ...prev, [permName]: !prev[permName] }))
}
// Parent izin mi?
function isParent(permission: PermissionWithStyle) {
return selectedGroupPermissions.some((p) => p.parentName === permission.name)
}
// Belirli parent'ın alt izinleri
function getChildren(parentName: string) {
if (!parentName) return []
return selectedGroupPermissions.filter((p) => p.parentName === parentName)
}
const providerName = 'R'
const { translate } = useLocalization()
const { getConfig } = useStoreActions((a) => a.abpConfig)
const [isLoading, setIsLoading] = useState(false)
const [permissionList, setPermissionList] = useState<GetPermissionListResultDto>()
const [selectedGroup, setSelectedGroup] = useState<PermissionGroupDto | undefined>()
const [selectedGroupPermissions, setSelectedGroupPermissions] = useState<PermissionWithStyle[]>(
[],
)
const [searchTerm, setSearchTerm] = useState('')
const mode = useStoreState((state) => state.theme.mode)
const tenant = useStoreState((state) => state.auth.tenant)
const { direction } = useConfig()
const className = `m${direction[0]}-`
const fetchDataPermissions = async () => {
if (!name) return
const response = await getPermissions(providerName, name)
const data = response.data
const tenantGroup = tenant?.menuGroup?.trim()
if (!tenantGroup) {
setPermissionList(data)
return
}
const filteredGroups = data.groups
.map((group: any) => {
const filteredPermissions = group.permissions.filter((perm: any) => {
if (!perm.menuGroup) return false
const groups =
typeof perm.menuGroup === 'string'
? perm.menuGroup.split('|').map((g: string) => g.trim())
: perm.menuGroup
return groups.includes(tenantGroup)
})
return { ...group, permissions: filteredPermissions }
})
.filter((group: any) => group.permissions.length > 0)
const filteredData = { ...data, groups: filteredGroups }
setPermissionList(filteredData)
}
const changeGroup = (groupName?: string) => {
const group = permissionList?.groups.find((a: any) => a.name === groupName)
if (!group) {
setSelectedGroup(undefined)
setSelectedGroupPermissions([])
return
}
setSelectedGroup(group)
const selectedGroupPerm = group.permissions.map(
(permission: any) =>
({
...permission,
class: className + findMargin(group.permissions, permission) * 4,
}) as PermissionWithStyle,
)
setSelectedGroupPermissions(selectedGroupPerm)
}
const onDialogOk = async () => {
try {
setIsLoading(true)
if (permissionList?.groups && name) {
const listPermissions = await getPermissions(providerName, name)
const unChangedPermissions = getPermissionsWithGroupName(listPermissions.data?.groups)
const changedPermissions = getPermissionsWithGroupName(permissionList.groups)
const updatePermList = changedPermissions
.filter(
(per) =>
(unChangedPermissions.find((unChanged) => unChanged.name === per.name) || {})
.isGranted !== per.isGranted,
)
.map(({ name, isGranted }) => ({ name, isGranted }))
// 🔴 KRİTİK NOKTA
await updatePermissions(providerName, name, {
permissions: updatePermList,
})
// ✅ artık backend kesin güncel
await getConfig(false)
toast.push(<Notification title={'Permission updated'} type="success" />, {
placement: 'top-end',
})
}
onDialogClose()
} catch (err) {
console.error(err)
} finally {
setIsLoading(false)
}
}
// --- Expand/Collapse All fonksiyonları (tüm state ve yardımcı fonksiyonlardan sonra) ---
const handleExpandAll = () => {
const expanded: OpenState = {}
function expandRecursively(perms: PermissionWithStyle[]) {
perms.forEach((perm) => {
if (isParent(perm)) {
expanded[perm.name || ''] = true
// Alt parent'ları da recursive aç
const children = getChildren(perm.name || '')
expandRecursively(children)
}
})
}
// Tüm parent ve alt parent'ları recursive olarak aç
expandRecursively(selectedGroupPermissions)
setOpenPermissions(expanded)
}
const handleCollapseAll = () => {
setOpenPermissions({})
}
function getPermissionsWithGroupName(groups: PermissionGroupDto[]): PermissionWithGroupName[] {
return groups.reduce(
(acc, val) => [
...acc,
...val.permissions.map<PermissionWithGroupName>((p: any) => ({
...p,
groupName: val.name || '',
})),
],
[] as PermissionWithGroupName[],
)
}
function findMargin(
permissions: PermissionGrantInfoDto[],
permission: PermissionGrantInfoDto,
level = 0,
): number {
const parentPermission = permissions.find((per) => per.name === permission.parentName)
if (parentPermission) {
return findMargin(permissions, parentPermission, level + 1)
} else {
return level
}
}
const isAllSelected = useMemo(
() =>
permissionList?.groups.every((group: any) =>
group.permissions.every((permission: any) => permission.isGranted),
),
[permissionList],
)
const isAllSelectedForGroup = useMemo(
() => selectedGroupPermissions.every((permission) => permission.isGranted),
[selectedGroupPermissions],
)
// Collapse/expand için recursive düzende izinleri döndür
const filteredPermissions = useMemo(() => {
const lowerTerm = searchTerm.toLowerCase()
// Filtreli veya tüm parent izinler
const parents = selectedGroupPermissions.filter(
(p) =>
!p.parentName &&
(searchTerm === '' ||
translate('::' + p.displayName)
.toLowerCase()
.includes(lowerTerm)),
)
// Recursive children builder (her seviye için doğru level)
function buildTree(
parent: PermissionWithStyle,
level: number,
): (PermissionWithStyle & { level: number })[] {
const arr = [{ ...parent, level }]
const parentKey = parent.name || ''
if (openPermissions[parentKey] || searchTerm) {
const children = getChildren(parentKey)
children.forEach((child) => {
arr.push(...buildTree(child, level + 1))
})
}
return arr
}
// Tüm parentlar ve altlarını sırayla ekle
let result: (PermissionWithStyle & { level: number })[] = []
parents.forEach((parent) => {
result = result.concat(buildTree(parent, 0))
})
return result
}, [selectedGroupPermissions, searchTerm, translate, openPermissions])
const onSelectAll = (value: boolean, e: ChangeEvent<HTMLInputElement>) => {
if (!permissionList) {
return
}
const currentGroupName = selectedGroup?.name
setSelectedGroup(undefined)
setSelectedGroupPermissions([])
if (e.target.name === 'group') {
permissionList?.groups
.find((group: any) => group.name === selectedGroup?.name)
?.permissions.forEach((permission: any) => {
permission.isGranted = value
})
} else {
permissionList?.groups.forEach((group: any) => {
group.permissions.forEach((permission: any) => {
permission.isGranted = value
})
})
}
setPermissionList({ ...permissionList })
changeGroup(currentGroupName)
}
function onClickCheckbox(clickedPermission: PermissionGrantInfoDto) {
if (!permissionList) {
return
}
const groupPerm = selectedGroupPermissions.map((per) => {
if (clickedPermission.name === per.name) {
return { ...per, isGranted: !per.isGranted }
} else if (clickedPermission.name === per.parentName && clickedPermission.isGranted) {
return { ...per, isGranted: false }
} else if (clickedPermission.parentName === per.name && !clickedPermission.isGranted) {
return { ...per, isGranted: true }
}
return per
})
const permGroup = permissionList?.groups.find((a: any) => a.name === selectedGroup?.name)
if (permGroup) {
permGroup.permissions = groupPerm
}
setPermissionList({ ...permissionList })
setSelectedGroupPermissions(groupPerm)
}
useEffect(() => {
fetchDataPermissions()
}, [name])
// --- Copy Permissions State and Logic ---
const [roleList, setRoleList] = useState<string[]>([])
// --- Copy Permissions Dialog State ---
const [copyDialogOpen, setCopyDialogOpen] = useState(false)
const [copyDialogRole, setCopyDialogRole] = useState('')
// Fetch all roles for select (except current)
useEffect(() => {
async function fetchRoles() {
try {
const res = await getRoles()
setRoleList(res.data.items?.map((r: any) => r.name) || [])
} catch (e) {
setRoleList([])
}
}
fetchRoles()
}, [copyDialogOpen])
// Dialog üzerinden kopyalama işlemi
const handleCopyDialogConfirm = async () => {
if (!copyDialogRole || !permissionList) return
setIsLoading(true)
try {
const res = await getPermissions(providerName, copyDialogRole)
const sourcePerms = getPermissionsWithGroupName(res.data?.groups)
const grantedNames = new Set(sourcePerms.filter((p) => p.isGranted).map((p) => p.name))
permissionList.groups.forEach((group) => {
group.permissions.forEach((perm) => {
perm.isGranted = grantedNames.has(perm.name)
})
})
setPermissionList({ ...permissionList })
changeGroup(selectedGroup?.name)
toast.push(<Notification title={translate('::PermissionsCopied')} type="success" />, {
placement: 'top-end',
})
setCopyDialogOpen(false)
setCopyDialogRole('')
} catch (e) {
toast.push(<Notification title={translate('::CopyFailed')} type="danger" />, {
placement: 'top-end',
})
} finally {
setIsLoading(false)
}
}
return permissionList ? (
<Container>
<Dialog
width="min(900px, 95vw)"
isOpen={open}
onAfterOpen={() => changeGroup(permissionList?.groups[0].name)}
onClose={onDialogClose}
onRequestClose={onDialogClose}
>
<Dialog.Body className="flex flex-col">
<PermissionDialogContent
name={name}
translate={translate}
permissionList={permissionList}
selectedGroup={selectedGroup}
selectedGroupPermissions={selectedGroupPermissions}
filteredPermissions={filteredPermissions}
isAllSelected={isAllSelected}
isAllSelectedForGroup={isAllSelectedForGroup}
isLoading={isLoading}
searchTerm={searchTerm}
openPermissions={openPermissions}
mode={mode}
onSelectAll={onSelectAll}
onDialogClose={onDialogClose}
onDialogOk={onDialogOk}
handleExpandAll={handleExpandAll}
handleCollapseAll={handleCollapseAll}
changeGroup={changeGroup}
togglePermission={togglePermission}
isParent={isParent}
getChildren={getChildren}
onClickCheckbox={onClickCheckbox}
setSearchTerm={setSearchTerm}
setCopyDialogOpen={setCopyDialogOpen}
/>
</Dialog.Body>
</Dialog>
{/* Copy Permissions Dialog */}
<Dialog
isOpen={copyDialogOpen}
width="min(400px, 95vw)"
onClose={() => setCopyDialogOpen(false)}
>
<h5 className="mb-2">{translate('::AbpIdentity.Roles.CopyPermissions')}</h5>
<div className="mb-4">
<select
className="border rounded px-2 py-1 w-full"
value={copyDialogRole}
onChange={(e) => setCopyDialogRole(e.target.value)}
>
<option value="">{translate('::App.Select')}</option>
{roleList
.filter((role) => role !== name)
.map((role) => (
<option key={role} value={role}>
{role}
</option>
))}
</select>
</div>
<div className="flex justify-end gap-2">
<Button variant="plain" onClick={() => setCopyDialogOpen(false)}>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
onClick={handleCopyDialogConfirm}
disabled={!copyDialogRole || isLoading}
loading={isLoading}
>
{translate('::Copy')}
</Button>
</div>
</Dialog>
</Container>
) : (
<></>
)
}
export default RolesPermission