sozsoft-platform/ui/src/views/admin/role-management/RolesPermission.tsx

606 lines
19 KiB
TypeScript
Raw Normal View History

2026-05-13 11:31:15 +00:00
import classNames from 'classnames'
import { FaChevronRight, FaChevronDown } from 'react-icons/fa'
2026-02-24 20:44:16 +00:00
import Container from '@/components/shared/Container'
import { Button, Checkbox, Dialog, Input, Menu, toast } from '@/components/ui'
import { useConfig } from '@/components/ui/ConfigProvider'
2026-05-13 11:31:15 +00:00
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
2026-02-24 20:44:16 +00:00
import Notification from '@/components/ui/Notification'
import {
GetPermissionListResultDto,
PermissionGrantInfoDto,
PermissionGroupDto,
PermissionWithGroupName,
PermissionWithStyle,
} from '@/proxy/admin/models'
2026-03-30 10:53:50 +00:00
import { getPermissions, getRoles, updatePermissions } from '@/services/identity.service'
2026-02-24 20:44:16 +00:00
import { useStoreActions, useStoreState } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { ChangeEvent, useEffect, useMemo, useState } from 'react'
type OpenState = Record<string, boolean>
2026-05-13 11:31:15 +00:00
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`}>
2026-05-19 20:22:25 +00:00
<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">
2026-05-13 11:31:15 +00:00
{isParentPerm ? (
<button
onClick={(e) => {
e.stopPropagation()
togglePermission(permKey)
}}
2026-05-19 20:22:25 +00:00
className="w-5 h-5 flex items-center justify-center rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition"
2026-05-13 11:31:15 +00:00
>
{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)}
>
2026-05-19 20:22:25 +00:00
<span className="text-sm text-gray-700 group-hover:text-gray-900 transition dark:text-gray-300 dark:group-hover:text-gray-100">
2026-05-13 11:31:15 +00:00
{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>
</>
)
}
2026-02-24 20:44:16 +00:00
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)
}
2026-02-24 20:44:16 +00:00
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) => {
2026-03-11 12:20:40 +00:00
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
2026-03-11 12:20:40 +00:00
return groups.includes(tenantGroup)
})
2026-02-24 20:44:16 +00:00
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)
2026-02-24 20:44:16 +00:00
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,
2026-02-24 20:44:16 +00:00
)
.map(({ name, isGranted }) => ({ name, isGranted }))
// 🔴 KRİTİK NOKTA
await updatePermissions(providerName, name, {
permissions: updatePermList,
})
// ✅ artık backend kesin güncel
await getConfig(false)
2026-02-24 20:44:16 +00:00
toast.push(<Notification title={'Permission updated'} type="success" />, {
placement: 'top-end',
})
}
onDialogClose()
} catch (err) {
console.error(err)
} finally {
2026-02-24 20:44:16 +00:00
setIsLoading(false)
}
2026-02-24 20:44:16 +00:00
}
// --- 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({})
}
2026-02-24 20:44:16 +00:00
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
2026-02-24 20:44:16 +00:00
const filteredPermissions = useMemo(() => {
const lowerTerm = searchTerm.toLowerCase()
// Filtreli veya tüm parent izinler
const parents = selectedGroupPermissions.filter(
2026-02-24 20:44:16 +00:00
(p) =>
!p.parentName &&
(searchTerm === '' ||
translate('::' + p.displayName)
.toLowerCase()
.includes(lowerTerm)),
2026-02-24 20:44:16 +00:00
)
// 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
}
2026-02-24 20:44:16 +00:00
// Tüm parentlar ve altlarını sırayla ekle
let result: (PermissionWithStyle & { level: number })[] = []
parents.forEach((parent) => {
result = result.concat(buildTree(parent, 0))
2026-02-24 20:44:16 +00:00
})
return result
}, [selectedGroupPermissions, searchTerm, translate, openPermissions])
2026-02-24 20:44:16 +00:00
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])
2026-03-30 10:53:50 +00:00
// --- 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)
}
}
2026-02-24 20:44:16 +00:00
return permissionList ? (
<Container>
<Dialog
2026-03-23 07:25:00 +00:00
width="min(900px, 95vw)"
2026-02-24 20:44:16 +00:00
isOpen={open}
onAfterOpen={() => changeGroup(permissionList?.groups[0].name)}
onClose={onDialogClose}
onRequestClose={onDialogClose}
>
2026-05-13 11:31:15 +00:00
<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>
2026-03-30 10:53:50 +00:00
</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)}>
2026-02-24 20:44:16 +00:00
{translate('::Cancel')}
</Button>
2026-03-30 10:53:50 +00:00
<Button
variant="solid"
onClick={handleCopyDialogConfirm}
disabled={!copyDialogRole || isLoading}
loading={isLoading}
>
{translate('::Copy')}
2026-02-24 20:44:16 +00:00
</Button>
</div>
</Dialog>
</Container>
) : (
<></>
)
}
export default RolesPermission