mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
refactor(console): role users transfer component (#2912)
This commit is contained in:
parent
17879ad293
commit
d5826ccd4d
13 changed files with 302 additions and 289 deletions
|
@ -1,135 +0,0 @@
|
||||||
import type { User } from '@logto/schemas';
|
|
||||||
import { conditional } from '@silverhand/essentials';
|
|
||||||
import type { ChangeEvent } from 'react';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
import Search from '@/assets/images/search.svg';
|
|
||||||
import type { RequestError } from '@/hooks/use-api';
|
|
||||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
|
||||||
import { buildUrl } from '@/utilities/url';
|
|
||||||
|
|
||||||
import Checkbox from '../Checkbox';
|
|
||||||
import Pagination from '../Pagination';
|
|
||||||
import TextInput from '../TextInput';
|
|
||||||
import UserAvatar from '../UserAvatar';
|
|
||||||
import * as styles from './index.module.scss';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
roleId: string;
|
|
||||||
onAddUser: (user: User) => void;
|
|
||||||
onRemoveUser: (user: User) => void;
|
|
||||||
selectedUsers: User[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageSize = 20;
|
|
||||||
const searchDelay = 500;
|
|
||||||
|
|
||||||
const SourceUsersBox = ({ roleId, selectedUsers, onAddUser, onRemoveUser }: Props) => {
|
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
|
||||||
const [pageIndex, setPageIndex] = useState(1);
|
|
||||||
const [keyword, setKeyword] = useState('');
|
|
||||||
const timerRef = useRef<NodeJS.Timeout>();
|
|
||||||
|
|
||||||
const clearTimer = () => {
|
|
||||||
if (timerRef.current) {
|
|
||||||
clearTimeout(timerRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
clearTimer();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const url = buildUrl('/api/users', {
|
|
||||||
excludeRoleId: roleId,
|
|
||||||
hideAdminUser: 'true',
|
|
||||||
page: String(pageIndex),
|
|
||||||
page_size: String(pageSize),
|
|
||||||
...conditional(keyword && { search: `%${keyword}%` }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data } = useSWR<[User[], number], RequestError>(url);
|
|
||||||
|
|
||||||
const [dataSource = [], totalCount] = data ?? [];
|
|
||||||
|
|
||||||
const handleSearchInput = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
clearTimer();
|
|
||||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
|
||||||
timerRef.current = setTimeout(() => {
|
|
||||||
setPageIndex(1);
|
|
||||||
setKeyword(event.target.value);
|
|
||||||
}, searchDelay);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isUserAdded = (user: User) => selectedUsers.findIndex(({ id }) => id === user.id) >= 0;
|
|
||||||
|
|
||||||
const onSelectUser = (user: User) => {
|
|
||||||
const userAdded = isUserAdded(user);
|
|
||||||
|
|
||||||
if (userAdded) {
|
|
||||||
onRemoveUser(user);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onAddUser(user);
|
|
||||||
};
|
|
||||||
|
|
||||||
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((user) => {
|
|
||||||
const added = isUserAdded(user);
|
|
||||||
const { id, name, avatar } = user;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={id}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
className={styles.item}
|
|
||||||
onKeyDown={onKeyDownHandler(() => {
|
|
||||||
onSelectUser(user);
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
onSelectUser(user);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={added}
|
|
||||||
disabled={false}
|
|
||||||
onChange={() => {
|
|
||||||
onSelectUser(user);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<UserAvatar className={styles.avatar} url={avatar} />
|
|
||||||
<div className={styles.name}>{name ?? t('users.unnamed')}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<Pagination
|
|
||||||
mode="pico"
|
|
||||||
pageIndex={pageIndex}
|
|
||||||
totalCount={totalCount}
|
|
||||||
pageSize={pageSize}
|
|
||||||
className={styles.pagination}
|
|
||||||
onChange={(page) => {
|
|
||||||
setPageIndex(page);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default SourceUsersBox;
|
|
|
@ -1,52 +0,0 @@
|
||||||
import type { User } from '@logto/schemas';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import Close from '@/assets/images/close.svg';
|
|
||||||
|
|
||||||
import IconButton from '../IconButton';
|
|
||||||
import UserAvatar from '../UserAvatar';
|
|
||||||
import * as styles from './index.module.scss';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
selectedUsers: User[];
|
|
||||||
onRemoveUser: (user: User) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TargetUsersBox = ({ selectedUsers, onRemoveUser }: Props) => {
|
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.box}>
|
|
||||||
<div className={classNames(styles.top, styles.added)}>
|
|
||||||
<span>
|
|
||||||
{`${selectedUsers.length} `}
|
|
||||||
{t('general.added')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.content}>
|
|
||||||
{selectedUsers.map((user) => {
|
|
||||||
const { id, avatar, name } = user;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={id} className={styles.item}>
|
|
||||||
<UserAvatar className={styles.avatar} url={avatar} />
|
|
||||||
<div className={styles.name}>{name ?? t('users.unnamed')}</div>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
iconClassName={styles.icon}
|
|
||||||
onClick={() => {
|
|
||||||
onRemoveUser(user);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Close />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TargetUsersBox;
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: _.unit(2.5) _.unit(4);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: _.unit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex: 1 1 0;
|
||||||
|
font: var(--font-body-medium);
|
||||||
|
@include _.text-ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-hover);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import type { User } from '@logto/schemas';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import Checkbox from '@/components/Checkbox';
|
||||||
|
import UserAvatar from '@/components/UserAvatar';
|
||||||
|
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: User;
|
||||||
|
isSelected: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SourceUserItem = ({ user: { avatar, name }, isSelected, onSelect }: Props) => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={styles.item}
|
||||||
|
onKeyDown={onKeyDownHandler(() => {
|
||||||
|
onSelect();
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={isSelected}
|
||||||
|
disabled={false}
|
||||||
|
onChange={() => {
|
||||||
|
onSelect();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<UserAvatar className={styles.avatar} url={avatar} />
|
||||||
|
<div className={styles.name}>{name ?? t('users.unnamed')}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SourceUserItem;
|
|
@ -0,0 +1,7 @@
|
||||||
|
.search {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
import type { User } 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 Pagination from '@/components/Pagination';
|
||||||
|
import TextInput from '@/components/TextInput';
|
||||||
|
import { defaultPageSize } from '@/consts';
|
||||||
|
import type { RequestError } from '@/hooks/use-api';
|
||||||
|
import useDebounce from '@/hooks/use-debounce';
|
||||||
|
import { formatKeyword } from '@/hooks/use-table-search-params';
|
||||||
|
import * as transferLayout from '@/scss/transfer.module.scss';
|
||||||
|
import { buildUrl } from '@/utilities/url';
|
||||||
|
|
||||||
|
import SourceUserItem from '../SourceUserItem';
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
roleId: string;
|
||||||
|
onChange: (value: User[]) => void;
|
||||||
|
selectedUsers: User[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchDelay = 500;
|
||||||
|
|
||||||
|
const SourceUsersBox = ({ roleId, selectedUsers, onChange }: Props) => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
const [pageIndex, setPageIndex] = useState(1);
|
||||||
|
const [keyword, setKeyword] = useState('');
|
||||||
|
const debounce = useDebounce();
|
||||||
|
|
||||||
|
const url = buildUrl('/api/users', {
|
||||||
|
excludeRoleId: roleId,
|
||||||
|
hideAdminUser: String(true),
|
||||||
|
page: String(pageIndex),
|
||||||
|
page_size: String(defaultPageSize),
|
||||||
|
...conditional(keyword && { search: formatKeyword(keyword) }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = useSWR<[User[], number], RequestError>(url);
|
||||||
|
|
||||||
|
const [dataSource = [], totalCount] = data ?? [];
|
||||||
|
|
||||||
|
const handleSearchInput = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
debounce(() => {
|
||||||
|
setPageIndex(1);
|
||||||
|
setKeyword(event.target.value);
|
||||||
|
}, searchDelay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUserAdded = (user: User) => selectedUsers.findIndex(({ id }) => id === user.id) >= 0;
|
||||||
|
|
||||||
|
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((user) => {
|
||||||
|
const isSelected = isUserAdded(user);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SourceUserItem
|
||||||
|
key={user.id}
|
||||||
|
user={user}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelect={() => {
|
||||||
|
onChange(
|
||||||
|
isSelected
|
||||||
|
? selectedUsers.filter(({ id }) => user.id !== id)
|
||||||
|
: [user, ...selectedUsers]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
mode="pico"
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
totalCount={totalCount}
|
||||||
|
pageSize={defaultPageSize}
|
||||||
|
className={transferLayout.boxPagination}
|
||||||
|
onChange={(page) => {
|
||||||
|
setPageIndex(page);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SourceUsersBox;
|
|
@ -0,0 +1,31 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: _.unit(2.5) _.unit(4);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: _.unit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex: 1 1 0;
|
||||||
|
font: var(--font-body-medium);
|
||||||
|
@include _.text-ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-hover);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import type { User } from '@logto/schemas';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import Close from '@/assets/images/close.svg';
|
||||||
|
import IconButton from '@/components/IconButton';
|
||||||
|
import UserAvatar from '@/components/UserAvatar';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: User;
|
||||||
|
onDelete: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TargetUserItem = ({ user: { avatar, name }, onDelete }: Props) => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.item}>
|
||||||
|
<UserAvatar className={styles.avatar} url={avatar} />
|
||||||
|
<div className={styles.name}>{name ?? t('users.unnamed')}</div>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
iconClassName={styles.icon}
|
||||||
|
onClick={() => {
|
||||||
|
onDelete();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TargetUserItem;
|
|
@ -0,0 +1,3 @@
|
||||||
|
.added {
|
||||||
|
font: var(--font-label-large);
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import type { User } from '@logto/schemas';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import * as transferLayout from '@/scss/transfer.module.scss';
|
||||||
|
|
||||||
|
import TargetUserItem from '../TargetUserItem';
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedUsers: User[];
|
||||||
|
onChange: (value: User[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TargetUsersBox = ({ selectedUsers, onChange }: Props) => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={transferLayout.box}>
|
||||||
|
<div className={transferLayout.boxTopBar}>
|
||||||
|
<span className={styles.added}>
|
||||||
|
{`${selectedUsers.length} `}
|
||||||
|
{t('general.added')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={transferLayout.boxContent}>
|
||||||
|
{selectedUsers.map((user) => (
|
||||||
|
<TargetUserItem
|
||||||
|
key={user.id}
|
||||||
|
user={user}
|
||||||
|
onDelete={() => {
|
||||||
|
onChange(selectedUsers.filter(({ id }) => id !== user.id));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TargetUsersBox;
|
|
@ -1,81 +1,5 @@
|
||||||
@use '@/scss/underscore' as _;
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
.container {
|
.roleUsersTransfer {
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: 6px;
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 360px;
|
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;
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: _.unit(2.5) _.unit(4);
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
object-fit: cover;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: _.unit(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
flex: 1 1 0;
|
|
||||||
font: var(--font-body-medium);
|
|
||||||
@include _.text-ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--color-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
height: 40px;
|
|
||||||
padding-right: _.unit(4);
|
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import type { User } from '@logto/schemas';
|
import type { User } from '@logto/schemas';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import SourceUsersBox from './SourceUsersBox';
|
import * as transferLayout from '@/scss/transfer.module.scss';
|
||||||
import TargetUsersBox from './TargetUsersBox';
|
|
||||||
|
import SourceUsersBox from './components/SourceUsersBox';
|
||||||
|
import TargetUsersBox from './components/TargetUsersBox';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -10,27 +13,12 @@ type Props = {
|
||||||
onChange: (value: User[]) => void;
|
onChange: (value: User[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoleUsersTransfer = ({ roleId, value, onChange }: Props) => {
|
const RoleUsersTransfer = ({ roleId, value, onChange }: Props) => (
|
||||||
const onAddUser = (user: User) => {
|
<div className={classNames(transferLayout.container, styles.roleUsersTransfer)}>
|
||||||
onChange([user, ...value]);
|
<SourceUsersBox roleId={roleId} selectedUsers={value} onChange={onChange} />
|
||||||
};
|
<div className={transferLayout.verticalBar} />
|
||||||
|
<TargetUsersBox selectedUsers={value} onChange={onChange} />
|
||||||
const onRemoveUser = (user: User) => {
|
</div>
|
||||||
onChange(value.filter(({ id }) => id !== user.id));
|
);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<SourceUsersBox
|
|
||||||
roleId={roleId}
|
|
||||||
selectedUsers={value}
|
|
||||||
onAddUser={onAddUser}
|
|
||||||
onRemoveUser={onRemoveUser}
|
|
||||||
/>
|
|
||||||
<div className={styles.verticalBar} />
|
|
||||||
<TargetUsersBox selectedUsers={value} onRemoveUser={onRemoveUser} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoleUsersTransfer;
|
export default RoleUsersTransfer;
|
||||||
|
|
|
@ -25,7 +25,7 @@ const AssignUsersModal = ({ roleId, isRemindSkip = false, onClose }: Props) => {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
const handleAssign = async () => {
|
const handleAssign = async () => {
|
||||||
if (users.length === 0) {
|
if (isLoading || users.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +70,7 @@ const AssignUsersModal = ({ roleId, isRemindSkip = false, onClose }: Props) => {
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
disabled={users.length === 0}
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
title="role_details.users.confirm_assign"
|
title="role_details.users.confirm_assign"
|
||||||
size="large"
|
size="large"
|
||||||
|
|
Loading…
Reference in a new issue