feat(users): add user permissions management and enhance theme switcher

This commit is contained in:
Tran Anh Tuan
2026-01-22 13:22:55 +07:00
parent 32a69cb190
commit 0bac8d0f25
22 changed files with 881 additions and 124 deletions

View File

@@ -2,17 +2,24 @@ import IconFont from '@/components/IconFont';
import TreeGroup from '@/components/shared/TreeGroup';
import { DEFAULT_PAGE_SIZE } from '@/constants';
import {
ROUTE_MANAGER_USERS,
ROUTE_MANAGER_USERS_PERMISSIONS,
} from '@/constants/routes';
import {
apiDeleteUser,
apiQueryUsers,
apiQueryUsersByGroup,
} from '@/services/master/UserController';
import { DeleteOutlined } from '@ant-design/icons';
import {
ActionType,
FooterToolbar,
ProCard,
ProColumns,
ProTable,
} from '@ant-design/pro-components';
import { FormattedMessage, useIntl } from '@umijs/max';
import { Button, Grid } from 'antd';
import { FormattedMessage, history, useIntl } from '@umijs/max';
import { Button, Grid, Popconfirm, theme } from 'antd';
import message from 'antd/es/message';
import Paragraph from 'antd/lib/typography/Paragraph';
import { useRef, useState } from 'react';
@@ -22,18 +29,21 @@ const ManagerUserPage = () => {
const { useBreakpoint } = Grid;
const intl = useIntl();
const screens = useBreakpoint();
const { token } = theme.useToken();
const actionRef = useRef<ActionType | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [messageApi, contextHolder] = message.useMessage();
const [selectedRowsState, setSelectedRowsState] = useState<
MasterModel.ProfileResponse[]
>([]);
const [groupCheckedKeys, setGroupCheckedKeys] = useState<
string | string[] | null
>(null);
const handleClickAssign = (user: MasterModel.ProfileResponse) => {
console.log('User ', user);
const path = `${ROUTE_MANAGER_USERS}/${user.id}/${ROUTE_MANAGER_USERS_PERMISSIONS}`;
history.push(path);
};
const columns: ProColumns<MasterModel.ProfileResponse>[] = [
@@ -53,6 +63,7 @@ const ManagerUserPage = () => {
marginBottom: 0,
verticalAlign: 'middle',
display: 'inline-block',
color: token.colorText,
}}
copyable
>
@@ -82,6 +93,7 @@ const ManagerUserPage = () => {
marginBottom: 0,
verticalAlign: 'middle',
display: 'inline-block',
color: token.colorText,
}}
copyable
>
@@ -124,6 +136,48 @@ const ManagerUserPage = () => {
},
];
const handleRemove = async (selectedRows: MasterModel.ProfileResponse[]) => {
const key = 'remove_user';
if (!selectedRows) return true;
try {
messageApi.open({
type: 'loading',
content: intl.formatMessage({
id: 'common.deleting',
defaultMessage: 'deleting...',
}),
duration: 0,
key,
});
const allDelete = selectedRows.map(
async (row: MasterModel.ProfileResponse) => {
await apiDeleteUser(row?.id || '');
},
);
await Promise.all(allDelete);
messageApi.open({
type: 'success',
content: intl.formatMessage({
id: 'master.users.delete.success',
defaultMessage: 'Deleted successfully and will refresh soon',
}),
key,
});
return true;
} catch (error) {
messageApi.open({
type: 'error',
content: intl.formatMessage({
id: 'master.users.delete.fail',
defaultMessage: 'Delete failed, please try again!',
}),
key,
});
return false;
}
};
return (
<>
{contextHolder}
@@ -182,56 +236,66 @@ const ManagerUserPage = () => {
const offset = current === 1 ? 0 : (current - 1) * size;
setIsLoading(true);
// If groups are checked, use queryUsersByGroup
if (groupCheckedKeys && groupCheckedKeys.length > 0) {
// Ensure groupCheckedKeys is an array
const groupIdsArray = Array.isArray(groupCheckedKeys)
? groupCheckedKeys.join(',')
: groupCheckedKeys;
try {
if (groupCheckedKeys && groupCheckedKeys.length > 0) {
// Ensure groupCheckedKeys is an array
const groupIdsArray = Array.isArray(groupCheckedKeys)
? groupCheckedKeys.join(',')
: groupCheckedKeys;
const userByGroupResponses = await apiQueryUsersByGroup(
groupIdsArray,
);
let users = userByGroupResponses.users || [];
// Apply filters
if (email) {
users = users.filter((user: MasterModel.ProfileResponse) =>
user.email?.includes(email),
const userByGroupResponses = await apiQueryUsersByGroup(
groupIdsArray,
);
}
if (phone_number) {
users = users.filter((user: MasterModel.ProfileResponse) =>
user.metadata?.phone_number?.includes(phone_number),
);
}
let users = userByGroupResponses.users || [];
// Apply filters
if (email) {
users = users.filter((user: MasterModel.ProfileResponse) =>
user.email?.includes(email),
);
}
if (phone_number) {
users = users.filter((user: MasterModel.ProfileResponse) =>
user.metadata?.phone_number?.includes(phone_number),
);
}
const total = users.length;
const paginatedUsers = users.slice(offset, offset + size);
const total = users.length;
const paginatedUsers = users.slice(offset, offset + size);
setIsLoading(false);
return {
data: paginatedUsers,
success: true,
total: total,
};
} else {
// Use regular queryUsers API
const metadata: Partial<MasterModel.ProfileMetadata> = {};
if (phone_number) metadata.phone_number = phone_number;
const query: MasterModel.SearchUserPaginationBody = {
offset: offset,
limit: size,
order: 'name',
dir: 'asc',
};
if (email) query.email = email;
if (Object.keys(metadata).length > 0)
query.metadata = metadata;
const response = await apiQueryUsers(query);
setIsLoading(false);
return {
data: response.users,
success: true,
total: response.total,
};
}
} catch (error) {
setIsLoading(false);
return {
data: paginatedUsers,
success: true,
total: total,
};
} else {
// Use regular queryUsers API
const metadata: Partial<MasterModel.ProfileMetadata> = {};
if (phone_number) metadata.phone_number = phone_number;
const query: MasterModel.SearchUserPaginationBody = {
offset: offset,
limit: size,
order: 'name',
dir: 'asc',
};
if (email) query.email = email;
if (Object.keys(metadata).length > 0) query.metadata = metadata;
const response = await apiQueryUsers(query);
setIsLoading(false);
return {
data: response.users,
success: true,
total: response.total,
data: [],
success: false,
total: 0,
};
}
}}
@@ -255,6 +319,53 @@ const ManagerUserPage = () => {
/>
</ProCard>
</ProCard>
{selectedRowsState?.length > 0 && (
<FooterToolbar
extra={
<div>
<FormattedMessage
id="master.footer.chosen"
defaultMessage="Chosen"
/>{' '}
<a
style={{
fontWeight: 600,
}}
>
{selectedRowsState.length}
</a>{' '}
<FormattedMessage
id="master.users.table.pagination"
defaultMessage="users"
/>
</div>
}
>
<Popconfirm
title={intl.formatMessage({
id: 'master.users.deletion.title',
defaultMessage: 'Are you sure to delete this selected items',
})}
okText={intl.formatMessage({
id: 'common.sure',
defaultMessage: 'Sure',
})}
onConfirm={async () => {
const success = await handleRemove(selectedRowsState);
if (success) {
setSelectedRowsState([]);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
>
<Button icon={<DeleteOutlined />} type="primary" danger>
<FormattedMessage id="common.delete" defaultMessage="Delete" />
</Button>
</Popconfirm>
</FooterToolbar>
)}
</>
);
};