mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(ui): Add sort options to user list (#913)
This commit is contained in:
@@ -2669,6 +2669,13 @@ paths:
|
||||
description: Returns all users in a JSON array.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- in: query
|
||||
name: sort
|
||||
schema:
|
||||
type: string
|
||||
enum: [created, updated, requests, displayname]
|
||||
default: created
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON array of all users
|
||||
|
@@ -11,10 +11,35 @@ import { UserType } from '../constants/user';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', async (_req, res) => {
|
||||
const userRepository = getRepository(User);
|
||||
router.get('/', async (req, res) => {
|
||||
let query = getRepository(User).createQueryBuilder('user');
|
||||
|
||||
const users = await userRepository.find();
|
||||
switch (req.query.sort) {
|
||||
case 'updated':
|
||||
query = query.orderBy('user.updatedAt', 'DESC');
|
||||
break;
|
||||
case 'displayname':
|
||||
query = query.orderBy(
|
||||
'(CASE WHEN user.username IS NULL THEN user.plexUsername ELSE user.username END)',
|
||||
'ASC'
|
||||
);
|
||||
break;
|
||||
case 'requests':
|
||||
query = query
|
||||
.addSelect((subQuery) => {
|
||||
return subQuery
|
||||
.select('COUNT(request.id)', 'requestCount')
|
||||
.from(MediaRequest, 'request')
|
||||
.where('request.requestedBy.id = user.id');
|
||||
}, 'requestCount')
|
||||
.orderBy('requestCount', 'DESC');
|
||||
break;
|
||||
default:
|
||||
query = query.orderBy('user.id', 'ASC');
|
||||
break;
|
||||
}
|
||||
|
||||
const users = await query.getMany();
|
||||
|
||||
return res.status(200).json(User.filterMany(users));
|
||||
});
|
||||
|
@@ -56,11 +56,11 @@ const RequestList: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={intl.formatMessage(messages.requests)} />
|
||||
<div className="flex flex-col justify-between md:items-end md:flex-row">
|
||||
<div className="flex flex-col justify-between lg:items-end lg:flex-row">
|
||||
<Header>{intl.formatMessage(messages.requests)}</Header>
|
||||
<div className="flex flex-col mt-2 md:flex-row">
|
||||
<div className="flex mb-2 md:mb-0 md:mr-2">
|
||||
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md sm:text-sm">
|
||||
<div className="flex flex-col flex-grow mt-2 sm:flex-row lg:flex-grow-0">
|
||||
<div className="flex flex-grow mb-2 sm:mb-0 sm:mr-2 lg:flex-grow-0">
|
||||
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="currentColor"
|
||||
@@ -81,12 +81,8 @@ const RequestList: React.FC = () => {
|
||||
setPageIndex(0);
|
||||
setCurrentFilter(e.target.value as Filter);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
setPageIndex(0);
|
||||
setCurrentFilter(e.target.value as Filter);
|
||||
}}
|
||||
value={currentFilter}
|
||||
className="rounded-r-only"
|
||||
className="text-sm rounded-r-only"
|
||||
>
|
||||
<option value="all">
|
||||
{intl.formatMessage(messages.filterAll)}
|
||||
@@ -99,8 +95,8 @@ const RequestList: React.FC = () => {
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md sm:text-sm">
|
||||
<div className="flex flex-grow mb-2 sm:mb-0 lg:flex-grow-0">
|
||||
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default sm:text-sm rounded-l-md">
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="currentColor"
|
||||
@@ -122,7 +118,7 @@ const RequestList: React.FC = () => {
|
||||
setCurrentSort(e.target.value as Sort);
|
||||
}}
|
||||
value={currentSort}
|
||||
className="rounded-r-only"
|
||||
className="text-sm rounded-r-only"
|
||||
>
|
||||
<option value="added">
|
||||
{intl.formatMessage(messages.sortAdded)}
|
||||
|
@@ -29,7 +29,7 @@ const messages = defineMessages({
|
||||
importfromplexerror: 'Something went wrong while importing users from Plex.',
|
||||
importedfromplex:
|
||||
'{userCount, plural, =0 {No new users} one {# new user} other {# new users}} imported from Plex.',
|
||||
username: 'Username',
|
||||
user: 'User',
|
||||
totalrequests: 'Total Requests',
|
||||
usertype: 'User Type',
|
||||
role: 'Role',
|
||||
@@ -39,7 +39,6 @@ const messages = defineMessages({
|
||||
bulkedit: 'Bulk Edit',
|
||||
delete: 'Delete',
|
||||
admin: 'Admin',
|
||||
user: 'User',
|
||||
plexuser: 'Plex User',
|
||||
deleteuser: 'Delete User',
|
||||
userdeleted: 'User deleted',
|
||||
@@ -62,13 +61,22 @@ const messages = defineMessages({
|
||||
'Email notifications need to be configured and enabled in order to automatically generate passwords.',
|
||||
autogeneratepassword: 'Automatically generate password',
|
||||
validationEmail: 'You must provide a valid email address',
|
||||
sortCreated: 'Creation Date',
|
||||
sortUpdated: 'Last Updated',
|
||||
sortDisplayName: 'Display Name',
|
||||
sortRequests: 'Request Count',
|
||||
});
|
||||
|
||||
type Sort = 'created' | 'updated' | 'requests' | 'displayname';
|
||||
|
||||
const UserList: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const router = useRouter();
|
||||
const { addToast } = useToasts();
|
||||
const { data, error, revalidate } = useSWR<User[]>('/api/v1/user');
|
||||
const [currentSort, setCurrentSort] = useState<Sort>('created');
|
||||
const { data, error, revalidate } = useSWR<User[]>(
|
||||
`/api/v1/user?sort=${currentSort}`
|
||||
);
|
||||
const [isDeleting, setDeleting] = useState(false);
|
||||
const [isImporting, setImporting] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState<{
|
||||
@@ -368,9 +376,10 @@ const UserList: React.FC = () => {
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<div className="flex flex-col justify-between md:items-end md:flex-row">
|
||||
<div className="flex flex-col justify-between lg:items-end lg:flex-row">
|
||||
<Header>{intl.formatMessage(messages.userlist)}</Header>
|
||||
<div className="flex flex-row justify-between mt-2 sm:flex-row md:mb-0">
|
||||
<div className="flex flex-col flex-grow mt-2 lg:flex-row lg:flex-grow-0">
|
||||
<div className="flex flex-row justify-between flex-grow mb-2 lg:mb-0 lg:flex-grow-0">
|
||||
<Button
|
||||
className="flex-grow mr-2 outline"
|
||||
buttonType="primary"
|
||||
@@ -379,7 +388,7 @@ const UserList: React.FC = () => {
|
||||
{intl.formatMessage(messages.createlocaluser)}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-grow outline"
|
||||
className="flex-grow outline lg:mr-2"
|
||||
buttonType="primary"
|
||||
disabled={isImporting}
|
||||
onClick={() => importFromPlex()}
|
||||
@@ -387,6 +396,41 @@ const UserList: React.FC = () => {
|
||||
{intl.formatMessage(messages.importfromplex)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-grow mb-2 lg:mb-0 lg:flex-grow-0">
|
||||
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M3 3a1 1 0 000 2h11a1 1 0 100-2H3zM3 7a1 1 0 000 2h7a1 1 0 100-2H3zM3 11a1 1 0 100 2h4a1 1 0 100-2H3zM15 8a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L15 13.586V8z" />
|
||||
</svg>
|
||||
</span>
|
||||
<select
|
||||
id="sort"
|
||||
name="sort"
|
||||
onChange={(e) => {
|
||||
setCurrentSort(e.target.value as Sort);
|
||||
}}
|
||||
value={currentSort}
|
||||
className="text-sm rounded-r-only"
|
||||
>
|
||||
<option value="created">
|
||||
{intl.formatMessage(messages.sortCreated)}
|
||||
</option>
|
||||
<option value="updated">
|
||||
{intl.formatMessage(messages.sortUpdated)}
|
||||
</option>
|
||||
<option value="requests">
|
||||
{intl.formatMessage(messages.sortRequests)}
|
||||
</option>
|
||||
<option value="displayname">
|
||||
{intl.formatMessage(messages.sortDisplayName)}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<thead>
|
||||
@@ -404,7 +448,7 @@ const UserList: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.username)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.user)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.totalrequests)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.usertype)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.role)}</Table.TH>
|
||||
|
@@ -628,6 +628,10 @@
|
||||
"components.UserList.passwordinfodescription": "Email notifications need to be configured and enabled in order to automatically generate passwords.",
|
||||
"components.UserList.plexuser": "Plex User",
|
||||
"components.UserList.role": "Role",
|
||||
"components.UserList.sortCreated": "Creation Date",
|
||||
"components.UserList.sortDisplayName": "Display Name",
|
||||
"components.UserList.sortRequests": "Request Count",
|
||||
"components.UserList.sortUpdated": "Last Updated",
|
||||
"components.UserList.totalrequests": "Total Requests",
|
||||
"components.UserList.user": "User",
|
||||
"components.UserList.usercreatedfailed": "Something went wrong while creating the user.",
|
||||
@@ -635,7 +639,6 @@
|
||||
"components.UserList.userdeleted": "User deleted.",
|
||||
"components.UserList.userdeleteerror": "Something went wrong while deleting the user.",
|
||||
"components.UserList.userlist": "User List",
|
||||
"components.UserList.username": "Username",
|
||||
"components.UserList.users": "Users",
|
||||
"components.UserList.userssaved": "Users saved!",
|
||||
"components.UserList.usertype": "User Type",
|
||||
|
Reference in New Issue
Block a user