0
Fork 0
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:
Xiao Yijun 2023-01-10 14:02:17 +08:00 committed by GitHub
parent 3ff6554c08
commit 8cda770ce9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 187 additions and 9 deletions

View file

@ -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}%`)}`
)}`
);

View file

@ -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>

View file

@ -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;

View file

@ -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);
}}
/>
)}
</>
);
};

View file

@ -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>
);
};

View file

@ -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;

View file

@ -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
},
};

View file

@ -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;

View file

@ -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!',
},
};

View file

@ -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;

View file

@ -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
},
};

View file

@ -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;

View file

@ -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
},
};

View file

@ -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;

View file

@ -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
},
};

View file

@ -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;

View file

@ -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
},
};

View file

@ -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;

View file

@ -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
},
};

View file

@ -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;

View file

@ -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
},
};