608 lines
20 KiB
TypeScript
608 lines
20 KiB
TypeScript
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
|