mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
refactor(console): role permissions transfer component (#2914)
This commit is contained in:
parent
f66fad07b9
commit
ce0b2c1cd2
21 changed files with 279 additions and 305 deletions
|
@ -1,116 +0,0 @@
|
|||
import type { ResourceResponse, ScopeResponse } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Search from '@/assets/images/search.svg';
|
||||
|
||||
import TextInput from '../TextInput';
|
||||
import ResourceItem from './components/ResourceItem';
|
||||
import * as styles from './index.module.scss';
|
||||
import type { DetailedResourceResponse } from './types';
|
||||
|
||||
type Props = {
|
||||
excludeScopeIds: string[];
|
||||
selectedPermissions: ScopeResponse[];
|
||||
onChange: (value: ScopeResponse[]) => void;
|
||||
};
|
||||
|
||||
const SourcePermissionsBox = ({ excludeScopeIds, selectedPermissions, onChange }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { data = [] } = useSWR<ResourceResponse[]>(`/api/resources?includeScopes=true`);
|
||||
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const handleSearchInput = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setKeyword(event.target.value);
|
||||
};
|
||||
|
||||
const isPermissionAdded = (scope: ScopeResponse) =>
|
||||
selectedPermissions.findIndex(({ id }) => id === scope.id) >= 0;
|
||||
|
||||
const onSelectPermission = (scope: ScopeResponse) => {
|
||||
const permissionAdded = isPermissionAdded(scope);
|
||||
|
||||
if (permissionAdded) {
|
||||
onChange(selectedPermissions.filter(({ id }) => id !== scope.id));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onChange([scope, ...selectedPermissions]);
|
||||
};
|
||||
|
||||
const onSelectResource = ({ scopes }: DetailedResourceResponse) => {
|
||||
const isAllSelected = scopes.every((scope) => isPermissionAdded(scope));
|
||||
const scopesIds = new Set(scopes.map(({ id }) => id));
|
||||
const basePermissions = selectedPermissions.filter(({ id }) => !scopesIds.has(id));
|
||||
|
||||
if (isAllSelected) {
|
||||
onChange(basePermissions);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onChange([...scopes, ...basePermissions]);
|
||||
};
|
||||
|
||||
const getResourceSelectedPermissions = ({ scopes }: DetailedResourceResponse) =>
|
||||
scopes.filter((scope) => selectedPermissions.findIndex(({ id }) => id === scope.id) >= 0);
|
||||
|
||||
const resources = data
|
||||
.filter(({ scopes }) => scopes.some(({ id }) => !excludeScopeIds.includes(id)))
|
||||
.map(({ scopes, ...resource }) => ({
|
||||
...resource,
|
||||
scopes: scopes
|
||||
.filter(({ id }) => !excludeScopeIds.includes(id))
|
||||
.map((scope) => ({
|
||||
...scope,
|
||||
resource,
|
||||
})),
|
||||
}));
|
||||
|
||||
const dataSource =
|
||||
conditional(
|
||||
keyword &&
|
||||
resources
|
||||
.filter(({ name, scopes }) => {
|
||||
return name.includes(keyword) || scopes.some(({ name }) => name.includes(keyword));
|
||||
})
|
||||
.map(({ scopes, ...resource }) => ({
|
||||
...resource,
|
||||
scopes: scopes.filter(
|
||||
({ name, resource }) => name.includes(keyword) || resource.name.includes(keyword)
|
||||
),
|
||||
}))
|
||||
.filter(({ scopes }) => scopes.length > 0)
|
||||
) ?? resources;
|
||||
|
||||
return (
|
||||
<div className={styles.box}>
|
||||
<div className={styles.top}>
|
||||
<TextInput
|
||||
className={styles.search}
|
||||
icon={<Search className={styles.icon} />}
|
||||
placeholder={t('general.search_placeholder')}
|
||||
onChange={handleSearchInput}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{dataSource.map((resource) => (
|
||||
<ResourceItem
|
||||
key={resource.id}
|
||||
resource={resource}
|
||||
selectedPermissions={getResourceSelectedPermissions(resource)}
|
||||
onSelectResource={onSelectResource}
|
||||
onSelectPermission={onSelectPermission}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourcePermissionsBox;
|
|
@ -1,38 +0,0 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import TargetPermissionItem from './components/TargetPermissionItem';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
selectedScopes: ScopeResponse[];
|
||||
onRemovePermission: (scope: ScopeResponse) => void;
|
||||
};
|
||||
|
||||
const TargetPermissionsBox = ({ selectedScopes, onRemovePermission }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return (
|
||||
<div className={styles.box}>
|
||||
<div className={classNames(styles.top, styles.added)}>
|
||||
<span>{t('role_details.permission.added_text', { value: selectedScopes.length })}</span>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{selectedScopes.map((scope) => {
|
||||
return (
|
||||
<TargetPermissionItem
|
||||
key={scope.id}
|
||||
scope={scope}
|
||||
onDelete={() => {
|
||||
onRemovePermission(scope);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetPermissionsBox;
|
|
@ -1,41 +0,0 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
|
||||
import Checkbox from '@/components/Checkbox';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
scope: ScopeResponse;
|
||||
isSelected: boolean;
|
||||
onSelectPermission: (scope: ScopeResponse) => void;
|
||||
};
|
||||
|
||||
const SourcePermissionItem = ({ scope, isSelected, onSelectPermission }: Props) => {
|
||||
const { name } = scope;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.sourcePermissionItem}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
onSelectPermission(scope);
|
||||
})}
|
||||
onClick={() => {
|
||||
onSelectPermission(scope);
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={false}
|
||||
onChange={() => {
|
||||
onSelectPermission(scope);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.name}>{name}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourcePermissionItem;
|
|
@ -1,47 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
height: 360px;
|
||||
|
||||
.verticalBar {
|
||||
@include _.vertical-bar;
|
||||
}
|
||||
|
||||
.box {
|
||||
flex: 1 1 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.top {
|
||||
height: 52px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 _.unit(4);
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.added {
|
||||
font: var(--font-label-large);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1 1 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
|
||||
import SourcePermissionsBox from './SourcePermissionsBox';
|
||||
import TargetPermissionsBox from './TargetPermissionsBox';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
value: ScopeResponse[];
|
||||
onChange: (value: ScopeResponse[]) => void;
|
||||
excludeScopeIds?: string[];
|
||||
};
|
||||
|
||||
const RolePermissionsTransfer = ({ excludeScopeIds = [], value, onChange }: Props) => (
|
||||
<div className={styles.container}>
|
||||
<SourcePermissionsBox
|
||||
excludeScopeIds={excludeScopeIds}
|
||||
selectedPermissions={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className={styles.verticalBar} />
|
||||
<TargetPermissionsBox
|
||||
selectedScopes={value}
|
||||
onRemovePermission={(scope) => {
|
||||
onChange(value.filter(({ id }) => id !== scope.id));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default RolePermissionsTransfer;
|
|
@ -24,7 +24,8 @@
|
|||
@include _.text-ellipsis;
|
||||
}
|
||||
|
||||
.permissionInfo {
|
||||
.scopeInfo {
|
||||
flex-shrink: 0;
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-text-secondary);
|
||||
margin-left: _.unit(2);
|
|
@ -9,25 +9,20 @@ import Checkbox from '@/components/Checkbox';
|
|||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import type { DetailedResourceResponse } from '../../types';
|
||||
import SourcePermissionItem from '../SourcePermissionItem';
|
||||
import SourceScopeItem from '../SourceScopeItem';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
resource: DetailedResourceResponse;
|
||||
selectedPermissions: ScopeResponse[];
|
||||
selectedScopes: ScopeResponse[];
|
||||
onSelectResource: (resource: DetailedResourceResponse) => void;
|
||||
onSelectPermission: (scope: ScopeResponse) => void;
|
||||
onSelectScope: (scope: ScopeResponse) => void;
|
||||
};
|
||||
|
||||
const ResourceItem = ({
|
||||
resource,
|
||||
selectedPermissions,
|
||||
onSelectResource,
|
||||
onSelectPermission,
|
||||
}: Props) => {
|
||||
const ResourceItem = ({ resource, selectedScopes, onSelectResource, onSelectScope }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { name, scopes } = resource;
|
||||
const selectedScopesCount = selectedPermissions.length;
|
||||
const selectedScopesCount = selectedScopes.length;
|
||||
const totalScopesCount = scopes.length;
|
||||
const [isScopesInvisible, setIsScopesInvisible] = useState(true);
|
||||
|
||||
|
@ -59,18 +54,18 @@ const ResourceItem = ({
|
|||
<CaretExpanded className={styles.caret} />
|
||||
)}
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div className={styles.permissionInfo}>
|
||||
<div className={styles.scopeInfo}>
|
||||
({t('role_details.permission.api_permission_count', { value: scopes.length })})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames(isScopesInvisible && styles.invisible)}>
|
||||
{scopes.map((scope) => (
|
||||
<SourcePermissionItem
|
||||
<SourceScopeItem
|
||||
key={scope.id}
|
||||
scope={scope}
|
||||
isSelected={selectedPermissions.findIndex(({ id }) => scope.id === id) >= 0}
|
||||
onSelectPermission={onSelectPermission}
|
||||
isSelected={selectedScopes.findIndex(({ id }) => scope.id === id) >= 0}
|
||||
onSelect={onSelectScope}
|
||||
/>
|
||||
))}
|
||||
</div>
|
|
@ -1,16 +1,16 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.sourcePermissionItem {
|
||||
.sourceScopeItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: _.unit(1.5) _.unit(7);
|
||||
cursor: pointer;
|
||||
|
||||
.name {
|
||||
padding: _.unit(1) _.unit(2);
|
||||
border-radius: 6px;
|
||||
background: var(--color-neutral-95);
|
||||
@include _.text-ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
|
@ -0,0 +1,39 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
|
||||
import Checkbox from '@/components/Checkbox';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
scope: ScopeResponse;
|
||||
isSelected: boolean;
|
||||
onSelect: (scope: ScopeResponse) => void;
|
||||
};
|
||||
|
||||
const SourceScopeItem = ({ scope, scope: { name }, isSelected, onSelect }: Props) => (
|
||||
<div className={styles.sourceScopeItem}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={false}
|
||||
onChange={() => {
|
||||
onSelect(scope);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={styles.name}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
onSelect(scope);
|
||||
})}
|
||||
onClick={() => {
|
||||
onSelect(scope);
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SourceScopeItem;
|
|
@ -0,0 +1,7 @@
|
|||
.search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
import type { ResourceResponse, Scope, ScopeResponse } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Search from '@/assets/images/search.svg';
|
||||
import type { DetailedResourceResponse } from '@/components/RoleScopesTransfer/types';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import * as transferLayout from '@/scss/transfer.module.scss';
|
||||
|
||||
import ResourceItem from '../ResourceItem';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
roleId?: string;
|
||||
selectedScopes: ScopeResponse[];
|
||||
onChange: (value: ScopeResponse[]) => void;
|
||||
};
|
||||
|
||||
const SourceScopesBox = ({ roleId, selectedScopes, onChange }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { data = [] } = useSWR<ResourceResponse[]>('/api/resources?includeScopes=true');
|
||||
const { data: roleScopes = [] } = useSWR<Scope[]>(roleId && `/api/roles/${roleId}/scopes`);
|
||||
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const handleSearchInput = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setKeyword(event.target.value);
|
||||
};
|
||||
|
||||
const isScopeAdded = (scope: ScopeResponse) =>
|
||||
selectedScopes.findIndex(({ id }) => id === scope.id) >= 0;
|
||||
|
||||
const onSelectScope = (scope: ScopeResponse) => {
|
||||
onChange(
|
||||
isScopeAdded(scope)
|
||||
? selectedScopes.filter(({ id }) => id !== scope.id)
|
||||
: [scope, ...selectedScopes]
|
||||
);
|
||||
};
|
||||
|
||||
const onSelectResource = ({ scopes: resourceScopes }: DetailedResourceResponse) => {
|
||||
const isAllSelected = resourceScopes.every((scope) => isScopeAdded(scope));
|
||||
const resourceScopesIds = new Set(resourceScopes.map(({ id }) => id));
|
||||
const selectedScopesExcludeResourceScopes = selectedScopes.filter(
|
||||
({ id }) => !resourceScopesIds.has(id)
|
||||
);
|
||||
onChange(
|
||||
isAllSelected
|
||||
? selectedScopesExcludeResourceScopes
|
||||
: [...resourceScopes, ...selectedScopesExcludeResourceScopes]
|
||||
);
|
||||
};
|
||||
|
||||
const getResourceSelectedScopes = ({ scopes }: DetailedResourceResponse) =>
|
||||
scopes.filter((scope) => selectedScopes.findIndex(({ id }) => id === scope.id) >= 0);
|
||||
|
||||
const resources: DetailedResourceResponse[] = useMemo(() => {
|
||||
const excludeScopeIds = new Set(roleScopes.map(({ id }) => id));
|
||||
|
||||
return data
|
||||
.filter(({ scopes }) => scopes.some(({ id }) => !excludeScopeIds.has(id)))
|
||||
.map(({ scopes, ...resource }) => ({
|
||||
...resource,
|
||||
scopes: scopes
|
||||
.filter(({ id }) => !excludeScopeIds.has(id))
|
||||
.map((scope) => ({
|
||||
...scope,
|
||||
resource,
|
||||
})),
|
||||
}));
|
||||
}, [data, roleScopes]);
|
||||
|
||||
const dataSource = useMemo(() => {
|
||||
const lowerCasedKeyword = keyword.toLowerCase();
|
||||
|
||||
return (
|
||||
conditional(
|
||||
lowerCasedKeyword &&
|
||||
resources
|
||||
.filter(({ name, scopes }) => {
|
||||
return (
|
||||
name.toLowerCase().includes(lowerCasedKeyword) ||
|
||||
scopes.some(({ name }) => name.toLowerCase().includes(lowerCasedKeyword))
|
||||
);
|
||||
})
|
||||
.map(({ scopes, ...resource }) => ({
|
||||
...resource,
|
||||
scopes: scopes.filter(
|
||||
({ name, resource }) =>
|
||||
name.toLocaleLowerCase().includes(lowerCasedKeyword) ||
|
||||
resource.name.toLocaleLowerCase().includes(lowerCasedKeyword)
|
||||
),
|
||||
}))
|
||||
.filter(({ scopes }) => scopes.length > 0)
|
||||
) ?? resources
|
||||
);
|
||||
}, [keyword, resources]);
|
||||
|
||||
return (
|
||||
<div className={transferLayout.box}>
|
||||
<div className={transferLayout.boxTopBar}>
|
||||
<TextInput
|
||||
className={styles.search}
|
||||
icon={<Search className={styles.icon} />}
|
||||
placeholder={t('general.search_placeholder')}
|
||||
onChange={handleSearchInput}
|
||||
/>
|
||||
</div>
|
||||
<div className={transferLayout.boxContent}>
|
||||
{dataSource.map((resource) => (
|
||||
<ResourceItem
|
||||
key={resource.id}
|
||||
resource={resource}
|
||||
selectedScopes={getResourceSelectedScopes(resource)}
|
||||
onSelectResource={onSelectResource}
|
||||
onSelectScope={onSelectScope}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourceScopesBox;
|
|
@ -1,6 +1,6 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.targetPermissionItem {
|
||||
.targetScopeItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: _.unit(1.5) _.unit(4);
|
||||
|
@ -28,4 +28,8 @@
|
|||
.icon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
import type { Key } from 'react';
|
||||
|
||||
import Close from '@/assets/images/close.svg';
|
||||
import IconButton from '@/components/IconButton';
|
||||
|
@ -7,28 +6,33 @@ import IconButton from '@/components/IconButton';
|
|||
import * as styles from './index.module.scss';
|
||||
|
||||
export type Props = {
|
||||
key: Key;
|
||||
scope: ScopeResponse;
|
||||
onDelete: () => void;
|
||||
onDelete: (scope: ScopeResponse) => void;
|
||||
};
|
||||
|
||||
const TargetPermissionItem = ({ key, scope, onDelete }: Props) => {
|
||||
const TargetScopeItem = ({ scope, onDelete }: Props) => {
|
||||
const {
|
||||
name,
|
||||
resource: { name: resourceName },
|
||||
} = scope;
|
||||
|
||||
return (
|
||||
<div key={key} className={styles.targetPermissionItem}>
|
||||
<div className={styles.targetScopeItem}>
|
||||
<div className={styles.title}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div className={styles.resourceName}>{`(${resourceName})`}</div>
|
||||
</div>
|
||||
<IconButton size="small" iconClassName={styles.icon} onClick={onDelete}>
|
||||
<IconButton
|
||||
size="small"
|
||||
iconClassName={styles.icon}
|
||||
onClick={() => {
|
||||
onDelete(scope);
|
||||
}}
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetPermissionItem;
|
||||
export default TargetScopeItem;
|
|
@ -0,0 +1,3 @@
|
|||
.added {
|
||||
font: var(--font-label-large);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as transferLayout from '@/scss/transfer.module.scss';
|
||||
|
||||
import TargetScopeItem from '../TargetScopeItem';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
selectedScopes: ScopeResponse[];
|
||||
onChange: (value: ScopeResponse[]) => void;
|
||||
};
|
||||
|
||||
const TargetScopesBox = ({ selectedScopes, onChange }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return (
|
||||
<div className={transferLayout.box}>
|
||||
<div className={transferLayout.boxTopBar}>
|
||||
<span className={styles.added}>
|
||||
{t('role_details.permission.added_text', { value: selectedScopes.length })}
|
||||
</span>
|
||||
</div>
|
||||
<div className={transferLayout.boxContent}>
|
||||
{selectedScopes.map((scope) => (
|
||||
<TargetScopeItem
|
||||
key={scope.id}
|
||||
scope={scope}
|
||||
onDelete={(scope) => {
|
||||
onChange(selectedScopes.filter(({ id }) => id !== scope.id));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetScopesBox;
|
|
@ -0,0 +1,5 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.roleScopesTransfer {
|
||||
height: 360px;
|
||||
}
|
24
packages/console/src/components/RoleScopesTransfer/index.tsx
Normal file
24
packages/console/src/components/RoleScopesTransfer/index.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import * as transferLayout from '@/scss/transfer.module.scss';
|
||||
|
||||
import SourceScopesBox from './components/SourceScopesBox';
|
||||
import TargetScopesBox from './components/TargetScopesBox';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
roleId?: string;
|
||||
value: ScopeResponse[];
|
||||
onChange: (value: ScopeResponse[]) => void;
|
||||
};
|
||||
|
||||
const RoleScopesTransfer = ({ roleId, value, onChange }: Props) => (
|
||||
<div className={classNames(transferLayout.container, styles.roleScopesTransfer)}>
|
||||
<SourceScopesBox roleId={roleId} selectedScopes={value} onChange={onChange} />
|
||||
<div className={transferLayout.verticalBar} />
|
||||
<TargetScopesBox selectedScopes={value} onChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default RoleScopesTransfer;
|
|
@ -7,17 +7,16 @@ import ReactModal from 'react-modal';
|
|||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import RolePermissionsTransfer from '@/components/RolePermissionsTransfer';
|
||||
import RoleScopesTransfer from '@/components/RoleScopesTransfer';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
type Props = {
|
||||
roleId: string;
|
||||
excludeScopeIds: string[];
|
||||
onClose: (success?: boolean) => void;
|
||||
};
|
||||
|
||||
const AssignPermissionsModal = ({ roleId, excludeScopeIds, onClose }: Props) => {
|
||||
const AssignPermissionsModal = ({ roleId, onClose }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
@ -71,9 +70,9 @@ const AssignPermissionsModal = ({ roleId, excludeScopeIds, onClose }: Props) =>
|
|||
onClose={onClose}
|
||||
>
|
||||
<FormField title="role_details.permission.assign_form_filed">
|
||||
<RolePermissionsTransfer
|
||||
<RoleScopesTransfer
|
||||
roleId={roleId}
|
||||
value={scopes}
|
||||
excludeScopeIds={excludeScopeIds}
|
||||
onChange={(scopes) => {
|
||||
setScopes(scopes);
|
||||
}}
|
||||
|
|
|
@ -81,7 +81,6 @@ const RolePermissions = () => {
|
|||
)}
|
||||
{isAssignPermissionsModalOpen && (
|
||||
<AssignPermissionsModal
|
||||
excludeScopeIds={scopes?.map(({ id }) => id) ?? []}
|
||||
roleId={roleId}
|
||||
onClose={(success) => {
|
||||
if (success) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Controller, useForm } from 'react-hook-form';
|
|||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import RolePermissionsTransfer from '@/components/RolePermissionsTransfer';
|
||||
import RoleScopesTransfer from '@/components/RoleScopesTransfer';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import useApi from '@/hooks/use-api';
|
||||
|
||||
|
@ -80,7 +80,7 @@ const CreateRoleForm = ({ onClose }: Props) => {
|
|||
name="scopes"
|
||||
defaultValue={[]}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<RolePermissionsTransfer value={value} onChange={onChange} />
|
||||
<RoleScopesTransfer value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
|
|
Loading…
Add table
Reference in a new issue