mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): role users transfer component (#2839)
This commit is contained in:
parent
2eb1cbbe10
commit
fc10ec312a
4 changed files with 292 additions and 0 deletions
|
@ -0,0 +1,129 @@
|
|||
import type { User } from '@logto/schemas';
|
||||
import { conditionalString } 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 Checkbox from '../Checkbox';
|
||||
import Pagination from '../Pagination';
|
||||
import TextInput from '../TextInput';
|
||||
import UserAvatar from '../UserAvatar';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
onAddUser: (user: User) => void;
|
||||
onRemoveUser: (user: User) => void;
|
||||
selectedUsers: User[];
|
||||
};
|
||||
|
||||
const pageSize = 20;
|
||||
const searchDelay = 500;
|
||||
|
||||
const SourceUsersBox = ({ 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 { data } = useSWR<[User[], number], RequestError>(
|
||||
`/api/users?page=${pageIndex}&page_size=${pageSize}&hideAdminUser=true${conditionalString(
|
||||
keyword && `&search=${encodeURIComponent(`%${keyword}%`)}`
|
||||
)}`
|
||||
);
|
||||
|
||||
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;
|
|
@ -0,0 +1,52 @@
|
|||
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,81 @@
|
|||
@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;
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
30
packages/console/src/components/RoleUsersTransfer/index.tsx
Normal file
30
packages/console/src/components/RoleUsersTransfer/index.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import type { User } from '@logto/schemas';
|
||||
|
||||
import SourceUsersBox from './SourceUsersBox';
|
||||
import TargetUsersBox from './TargetUsersBox';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
value: User[];
|
||||
onChange: (value: User[]) => void;
|
||||
};
|
||||
|
||||
const RoleUsersTransfer = ({ value, onChange }: Props) => {
|
||||
const onAddUser = (user: User) => {
|
||||
onChange([user, ...value]);
|
||||
};
|
||||
|
||||
const onRemoveUser = (user: User) => {
|
||||
onChange(value.filter(({ id }) => id !== user.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<SourceUsersBox selectedUsers={value} onAddUser={onAddUser} onRemoveUser={onRemoveUser} />
|
||||
<div className={styles.verticalBar} />
|
||||
<TargetUsersBox selectedUsers={value} onRemoveUser={onRemoveUser} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleUsersTransfer;
|
Loading…
Reference in a new issue