mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
feat(console): assign users to a role (#2883)
This commit is contained in:
parent
3ff6554c08
commit
8cda770ce9
21 changed files with 187 additions and 9 deletions
|
@ -16,6 +16,7 @@ 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[];
|
||||
|
@ -24,7 +25,7 @@ type Props = {
|
|||
const pageSize = 20;
|
||||
const searchDelay = 500;
|
||||
|
||||
const SourceUsersBox = ({ selectedUsers, onAddUser, onRemoveUser }: Props) => {
|
||||
const SourceUsersBox = ({ roleId, selectedUsers, onAddUser, onRemoveUser }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [pageIndex, setPageIndex] = useState(1);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
@ -43,7 +44,7 @@ const SourceUsersBox = ({ selectedUsers, onAddUser, onRemoveUser }: Props) => {
|
|||
}, []);
|
||||
|
||||
const { data } = useSWR<[User[], number], RequestError>(
|
||||
`/api/users?page=${pageIndex}&page_size=${pageSize}&hideAdminUser=true${conditionalString(
|
||||
`/api/users?excludeRoleId=${roleId}&page=${pageIndex}&page_size=${pageSize}&hideAdminUser=true${conditionalString(
|
||||
keyword && `&search=${encodeURIComponent(`%${keyword}%`)}`
|
||||
)}`
|
||||
);
|
||||
|
|
|
@ -5,11 +5,12 @@ import TargetUsersBox from './TargetUsersBox';
|
|||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
roleId: string;
|
||||
value: User[];
|
||||
onChange: (value: User[]) => void;
|
||||
};
|
||||
|
||||
const RoleUsersTransfer = ({ value, onChange }: Props) => {
|
||||
const RoleUsersTransfer = ({ roleId, value, onChange }: Props) => {
|
||||
const onAddUser = (user: User) => {
|
||||
onChange([user, ...value]);
|
||||
};
|
||||
|
@ -20,7 +21,12 @@ const RoleUsersTransfer = ({ value, onChange }: Props) => {
|
|||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<SourceUsersBox selectedUsers={value} onAddUser={onAddUser} onRemoveUser={onRemoveUser} />
|
||||
<SourceUsersBox
|
||||
roleId={roleId}
|
||||
selectedUsers={value}
|
||||
onAddUser={onAddUser}
|
||||
onRemoveUser={onRemoveUser}
|
||||
/>
|
||||
<div className={styles.verticalBar} />
|
||||
<TargetUsersBox selectedUsers={value} onRemoveUser={onRemoveUser} />
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import type { User } from '@logto/schemas';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import RoleUsersTransfer from '@/components/RoleUsersTransfer';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
type Props = {
|
||||
roleId: string;
|
||||
isRemindSkip?: boolean;
|
||||
onClose: (success?: boolean) => void;
|
||||
};
|
||||
|
||||
const AssignUsersModal = ({ roleId, isRemindSkip = false, onClose }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
|
||||
const api = useApi();
|
||||
|
||||
const handleAssign = async () => {
|
||||
if (users.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await api.post(`/api/roles/${roleId}/users`, {
|
||||
json: { userIds: users.map(({ id }) => id) },
|
||||
});
|
||||
toast.success(t('role_details.users.users_assigned'));
|
||||
onClose(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen
|
||||
shouldCloseOnEsc
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
onRequestClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ModalLayout
|
||||
title="role_details.users.assign_title"
|
||||
subtitle="role_details.users.assign_subtitle"
|
||||
size="large"
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
htmlType="submit"
|
||||
title={isRemindSkip ? 'general.skip_for_now' : 'general.cancel'}
|
||||
size="large"
|
||||
type="default"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
htmlType="submit"
|
||||
title="role_details.users.confirm_assign"
|
||||
size="large"
|
||||
type="primary"
|
||||
onClick={handleAssign}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<FormField title="role_details.users.assign_users_field">
|
||||
<RoleUsersTransfer
|
||||
roleId={roleId}
|
||||
value={users}
|
||||
onChange={(value) => {
|
||||
setUsers(value);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
</ModalLayout>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssignUsersModal;
|
|
@ -20,6 +20,7 @@ import type { RequestError } from '@/hooks/use-api';
|
|||
import useApi from '@/hooks/use-api';
|
||||
|
||||
import type { RoleDetailsOutletContext } from '../types';
|
||||
import AssignUsersModal from './components/AssignUsersModal';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const RoleUsers = () => {
|
||||
|
@ -37,6 +38,7 @@ const RoleUsers = () => {
|
|||
|
||||
const isLoading = !users && !error;
|
||||
|
||||
const [isAssignModalOpen, setIsAssignModalOpen] = useState(false);
|
||||
const [userToBeDeleted, setUserToBeDeleted] = useState<User>();
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
|
@ -117,7 +119,7 @@ const RoleUsers = () => {
|
|||
size="large"
|
||||
icon={<Plus />}
|
||||
onClick={() => {
|
||||
// TODO @xiaoyijun assign users to role
|
||||
setIsAssignModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -128,7 +130,7 @@ const RoleUsers = () => {
|
|||
title="role_details.users.assign_button"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
// TODO @xiaoyijun assign users to role
|
||||
setIsAssignModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
@ -147,6 +149,17 @@ const RoleUsers = () => {
|
|||
{t('role_details.users.delete_description')}
|
||||
</ConfirmModal>
|
||||
)}
|
||||
{isAssignModalOpen && (
|
||||
<AssignUsersModal
|
||||
roleId={roleId}
|
||||
onClose={(success) => {
|
||||
if (success) {
|
||||
void mutate();
|
||||
}
|
||||
setIsAssignModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import type { Role } from '@logto/schemas';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import AssignUsersModal from '@/pages/RoleDetails/RoleUsers/components/AssignUsersModal';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import type { Props as CreateRoleFormProps } from '../CreateRoleForm';
|
||||
|
@ -17,10 +19,11 @@ const CreateRoleModal = ({ onClose }: Props) => {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [createdRole, setCreatedRole] = useState<Role>();
|
||||
|
||||
const onCreateFormClose: CreateRoleFormProps['onClose'] = (createdRole?: Role) => {
|
||||
if (createdRole) {
|
||||
// TODO @xiaoyijun open assigning role to users modal
|
||||
navigate(`/roles/${createdRole.id}`, { replace: true });
|
||||
setCreatedRole(createdRole);
|
||||
toast.success(t('roles.role_created', { name: createdRole.name }));
|
||||
|
||||
return;
|
||||
|
@ -37,7 +40,17 @@ const CreateRoleModal = ({ onClose }: Props) => {
|
|||
overlayClassName={modalStyles.overlay}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<CreateRoleForm onClose={onCreateFormClose} />
|
||||
{createdRole ? (
|
||||
<AssignUsersModal
|
||||
isRemindSkip
|
||||
roleId={createdRole.id}
|
||||
onClose={() => {
|
||||
navigate(`/roles/${createdRole.id}`, { replace: true });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CreateRoleForm onClose={onCreateFormClose} />
|
||||
)}
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -42,6 +42,7 @@ const general = {
|
|||
page_info: '{{min, number}}-{{max, number}} of {{total, number}}', // UNTRANSLATED
|
||||
learn_more: 'Learn more', // UNTRANSLATED
|
||||
tab_errors: '{{count, number}} errors', // UNTRANSLATED
|
||||
skip_for_now: 'Skip for now', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -35,6 +35,11 @@ const role_details = {
|
|||
delete_description:
|
||||
'It will remain in your user pool but lose the authorization for this role.', // UNTRANSLATED
|
||||
deleted: 'The user {{name}} has been successfully deleted from this role.', // UNTRANSLATED
|
||||
assign_title: 'Assign users', // UNTRANSLATED
|
||||
assign_subtitle: 'Assign users to the role', // UNTRANSLATED
|
||||
assign_users_field: 'Assign users', // UNTRANSLATED
|
||||
confirm_assign: 'Assign users', // UNTRANSLATED
|
||||
users_assigned: 'The selected users were successfully assigned to this role!', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ const general = {
|
|||
page_info: '{{min, number}}-{{max, number}} of {{total, number}}',
|
||||
learn_more: 'Learn more',
|
||||
tab_errors: '{{count, number}} errors',
|
||||
skip_for_now: 'Skip for now',
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -35,6 +35,11 @@ const role_details = {
|
|||
delete_description:
|
||||
'It will remain in your user pool but lose the authorization for this role.',
|
||||
deleted: 'The user {{name}} has been successfully deleted from this role.', // UNTRANSLATED
|
||||
assign_title: 'Assign users',
|
||||
assign_subtitle: 'Assign users to the role',
|
||||
assign_users_field: 'Assign users',
|
||||
confirm_assign: 'Assign users',
|
||||
users_assigned: 'The selected users were successfully assigned to this role!',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ const general = {
|
|||
page_info: '{{min, number}}-{{max, number}} of {{total, number}}', // UNTRANSLATED
|
||||
learn_more: 'Learn more', // UNTRANSLATED
|
||||
tab_errors: '{{count, number}} errors', // UNTRANSLATED
|
||||
skip_for_now: 'Skip for now', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -35,6 +35,11 @@ const role_details = {
|
|||
delete_description:
|
||||
'It will remain in your user pool but lose the authorization for this role.', // UNTRANSLATED
|
||||
deleted: 'The user {{name}} has been successfully deleted from this role.', // UNTRANSLATED
|
||||
assign_title: 'Assign users', // UNTRANSLATED
|
||||
assign_subtitle: 'Assign users to the role', // UNTRANSLATED
|
||||
assign_users_field: 'Assign users', // UNTRANSLATED
|
||||
confirm_assign: 'Assign users', // UNTRANSLATED
|
||||
users_assigned: 'The selected users were successfully assigned to this role!', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ const general = {
|
|||
page_info: '{{min, number}}-{{max, number}} / {{total, number}}',
|
||||
learn_more: '더 알아보기',
|
||||
tab_errors: '{{count, number}} 오류',
|
||||
skip_for_now: 'Skip for now', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -35,6 +35,11 @@ const role_details = {
|
|||
delete_description:
|
||||
'It will remain in your user pool but lose the authorization for this role.', // UNTRANSLATED
|
||||
deleted: 'The user {{name}} has been successfully deleted from this role.', // UNTRANSLATED
|
||||
assign_title: 'Assign users', // UNTRANSLATED
|
||||
assign_subtitle: 'Assign users to the role', // UNTRANSLATED
|
||||
assign_users_field: 'Assign users', // UNTRANSLATED
|
||||
confirm_assign: 'Assign users', // UNTRANSLATED
|
||||
users_assigned: 'The selected users were successfully assigned to this role!', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ const general = {
|
|||
page_info: '{{min, number}}-{{max, number}} de {{total, number}}',
|
||||
learn_more: 'Saber mais',
|
||||
tab_errors: '{{count, number}} erros',
|
||||
skip_for_now: 'Skip for now', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -35,6 +35,11 @@ const role_details = {
|
|||
delete_description:
|
||||
'It will remain in your user pool but lose the authorization for this role.', // UNTRANSLATED
|
||||
deleted: 'The user {{name}} has been successfully deleted from this role.', // UNTRANSLATED
|
||||
assign_title: 'Assign users', // UNTRANSLATED
|
||||
assign_subtitle: 'Assign users to the role', // UNTRANSLATED
|
||||
assign_users_field: 'Assign users', // UNTRANSLATED
|
||||
confirm_assign: 'Assign users', // UNTRANSLATED
|
||||
users_assigned: 'The selected users were successfully assigned to this role!', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ const general = {
|
|||
page_info: '{{min, number}}-{{max, number}} of {{total, number}}', // UNTRANSLATED
|
||||
learn_more: 'Learn more', // UNTRANSLATED
|
||||
tab_errors: '{{count, number}} errors', // UNTRANSLATED
|
||||
skip_for_now: 'Skip for now', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -35,6 +35,11 @@ const role_details = {
|
|||
delete_description:
|
||||
'It will remain in your user pool but lose the authorization for this role.', // UNTRANSLATED
|
||||
deleted: 'The user {{name}} has been successfully deleted from this role.', // UNTRANSLATED
|
||||
assign_title: 'Assign users', // UNTRANSLATED
|
||||
assign_subtitle: 'Assign users to the role', // UNTRANSLATED
|
||||
assign_users_field: 'Assign users', // UNTRANSLATED
|
||||
confirm_assign: 'Assign users', // UNTRANSLATED
|
||||
users_assigned: 'The selected users were successfully assigned to this role!', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ const general = {
|
|||
page_info: '{{min, number}}-{{max, number}} of {{total, number}}', // UNTRANSLATED
|
||||
learn_more: 'Learn more', // UNTRANSLATED
|
||||
tab_errors: '{{count, number}} errors', // UNTRANSLATED
|
||||
skip_for_now: 'Skip for now', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -35,6 +35,11 @@ const role_details = {
|
|||
delete_description:
|
||||
'It will remain in your user pool but lose the authorization for this role.', // UNTRANSLATED
|
||||
deleted: 'The user {{name}} has been successfully deleted from this role.', // UNTRANSLATED
|
||||
assign_title: 'Assign users', // UNTRANSLATED
|
||||
assign_subtitle: 'Assign users to the role', // UNTRANSLATED
|
||||
assign_users_field: 'Assign users', // UNTRANSLATED
|
||||
confirm_assign: 'Assign users', // UNTRANSLATED
|
||||
users_assigned: 'The selected users were successfully assigned to this role!', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ const general = {
|
|||
page_info: '{{min, number}}-{{max, number}} 共 {{total, number}} 条', // UNTRANSLATED
|
||||
learn_more: 'Learn more', // UNTRANSLATED
|
||||
tab_errors: '{{count, number}} errors', // UNTRANSLATED
|
||||
skip_for_now: 'Skip for now', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -35,6 +35,11 @@ const role_details = {
|
|||
delete_description:
|
||||
'It will remain in your user pool but lose the authorization for this role.', // UNTRANSLATED
|
||||
deleted: 'The user {{name}} has been successfully deleted from this role.', // UNTRANSLATED
|
||||
assign_title: 'Assign users', // UNTRANSLATED
|
||||
assign_subtitle: 'Assign users to the role', // UNTRANSLATED
|
||||
assign_users_field: 'Assign users', // UNTRANSLATED
|
||||
confirm_assign: 'Assign users', // UNTRANSLATED
|
||||
users_assigned: 'The selected users were successfully assigned to this role!', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue