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.
|
description: Returns all users in a JSON array.
|
||||||
tags:
|
tags:
|
||||||
- users
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: sort
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [created, updated, requests, displayname]
|
||||||
|
default: created
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: A JSON array of all users
|
description: A JSON array of all users
|
||||||
|
@@ -11,10 +11,35 @@ import { UserType } from '../constants/user';
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/', async (_req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const userRepository = getRepository(User);
|
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));
|
return res.status(200).json(User.filterMany(users));
|
||||||
});
|
});
|
||||||
|
@@ -56,11 +56,11 @@ const RequestList: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle title={intl.formatMessage(messages.requests)} />
|
<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>
|
<Header>{intl.formatMessage(messages.requests)}</Header>
|
||||||
<div className="flex flex-col mt-2 md:flex-row">
|
<div className="flex flex-col flex-grow mt-2 sm:flex-row lg:flex-grow-0">
|
||||||
<div className="flex mb-2 md:mb-0 md:mr-2">
|
<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-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md sm:text-sm">
|
<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
|
<svg
|
||||||
className="w-6 h-6"
|
className="w-6 h-6"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -81,12 +81,8 @@ const RequestList: React.FC = () => {
|
|||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
setCurrentFilter(e.target.value as Filter);
|
setCurrentFilter(e.target.value as Filter);
|
||||||
}}
|
}}
|
||||||
onBlur={(e) => {
|
|
||||||
setPageIndex(0);
|
|
||||||
setCurrentFilter(e.target.value as Filter);
|
|
||||||
}}
|
|
||||||
value={currentFilter}
|
value={currentFilter}
|
||||||
className="rounded-r-only"
|
className="text-sm rounded-r-only"
|
||||||
>
|
>
|
||||||
<option value="all">
|
<option value="all">
|
||||||
{intl.formatMessage(messages.filterAll)}
|
{intl.formatMessage(messages.filterAll)}
|
||||||
@@ -99,8 +95,8 @@ const RequestList: React.FC = () => {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<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 rounded-l-md sm:text-sm">
|
<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
|
<svg
|
||||||
className="w-6 h-6"
|
className="w-6 h-6"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -122,7 +118,7 @@ const RequestList: React.FC = () => {
|
|||||||
setCurrentSort(e.target.value as Sort);
|
setCurrentSort(e.target.value as Sort);
|
||||||
}}
|
}}
|
||||||
value={currentSort}
|
value={currentSort}
|
||||||
className="rounded-r-only"
|
className="text-sm rounded-r-only"
|
||||||
>
|
>
|
||||||
<option value="added">
|
<option value="added">
|
||||||
{intl.formatMessage(messages.sortAdded)}
|
{intl.formatMessage(messages.sortAdded)}
|
||||||
|
@@ -29,7 +29,7 @@ const messages = defineMessages({
|
|||||||
importfromplexerror: 'Something went wrong while importing users from Plex.',
|
importfromplexerror: 'Something went wrong while importing users from Plex.',
|
||||||
importedfromplex:
|
importedfromplex:
|
||||||
'{userCount, plural, =0 {No new users} one {# new user} other {# new users}} imported from Plex.',
|
'{userCount, plural, =0 {No new users} one {# new user} other {# new users}} imported from Plex.',
|
||||||
username: 'Username',
|
user: 'User',
|
||||||
totalrequests: 'Total Requests',
|
totalrequests: 'Total Requests',
|
||||||
usertype: 'User Type',
|
usertype: 'User Type',
|
||||||
role: 'Role',
|
role: 'Role',
|
||||||
@@ -39,7 +39,6 @@ const messages = defineMessages({
|
|||||||
bulkedit: 'Bulk Edit',
|
bulkedit: 'Bulk Edit',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
admin: 'Admin',
|
admin: 'Admin',
|
||||||
user: 'User',
|
|
||||||
plexuser: 'Plex User',
|
plexuser: 'Plex User',
|
||||||
deleteuser: 'Delete User',
|
deleteuser: 'Delete User',
|
||||||
userdeleted: 'User deleted',
|
userdeleted: 'User deleted',
|
||||||
@@ -62,13 +61,22 @@ const messages = defineMessages({
|
|||||||
'Email notifications need to be configured and enabled in order to automatically generate passwords.',
|
'Email notifications need to be configured and enabled in order to automatically generate passwords.',
|
||||||
autogeneratepassword: 'Automatically generate password',
|
autogeneratepassword: 'Automatically generate password',
|
||||||
validationEmail: 'You must provide a valid email address',
|
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 UserList: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { addToast } = useToasts();
|
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 [isDeleting, setDeleting] = useState(false);
|
||||||
const [isImporting, setImporting] = useState(false);
|
const [isImporting, setImporting] = useState(false);
|
||||||
const [deleteModal, setDeleteModal] = useState<{
|
const [deleteModal, setDeleteModal] = useState<{
|
||||||
@@ -368,24 +376,60 @@ const UserList: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Transition>
|
</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>
|
<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">
|
||||||
<Button
|
<div className="flex flex-row justify-between flex-grow mb-2 lg:mb-0 lg:flex-grow-0">
|
||||||
className="flex-grow mr-2 outline"
|
<Button
|
||||||
buttonType="primary"
|
className="flex-grow mr-2 outline"
|
||||||
onClick={() => setCreateModal({ isOpen: true })}
|
buttonType="primary"
|
||||||
>
|
onClick={() => setCreateModal({ isOpen: true })}
|
||||||
{intl.formatMessage(messages.createlocaluser)}
|
>
|
||||||
</Button>
|
{intl.formatMessage(messages.createlocaluser)}
|
||||||
<Button
|
</Button>
|
||||||
className="flex-grow outline"
|
<Button
|
||||||
buttonType="primary"
|
className="flex-grow outline lg:mr-2"
|
||||||
disabled={isImporting}
|
buttonType="primary"
|
||||||
onClick={() => importFromPlex()}
|
disabled={isImporting}
|
||||||
>
|
onClick={() => importFromPlex()}
|
||||||
{intl.formatMessage(messages.importfromplex)}
|
>
|
||||||
</Button>
|
{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>
|
||||||
</div>
|
</div>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -404,7 +448,7 @@ const UserList: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Table.TH>
|
</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.totalrequests)}</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.usertype)}</Table.TH>
|
<Table.TH>{intl.formatMessage(messages.usertype)}</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.role)}</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.passwordinfodescription": "Email notifications need to be configured and enabled in order to automatically generate passwords.",
|
||||||
"components.UserList.plexuser": "Plex User",
|
"components.UserList.plexuser": "Plex User",
|
||||||
"components.UserList.role": "Role",
|
"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.totalrequests": "Total Requests",
|
||||||
"components.UserList.user": "User",
|
"components.UserList.user": "User",
|
||||||
"components.UserList.usercreatedfailed": "Something went wrong while creating the user.",
|
"components.UserList.usercreatedfailed": "Something went wrong while creating the user.",
|
||||||
@@ -635,7 +639,6 @@
|
|||||||
"components.UserList.userdeleted": "User deleted.",
|
"components.UserList.userdeleted": "User deleted.",
|
||||||
"components.UserList.userdeleteerror": "Something went wrong while deleting the user.",
|
"components.UserList.userdeleteerror": "Something went wrong while deleting the user.",
|
||||||
"components.UserList.userlist": "User List",
|
"components.UserList.userlist": "User List",
|
||||||
"components.UserList.username": "Username",
|
|
||||||
"components.UserList.users": "Users",
|
"components.UserList.users": "Users",
|
||||||
"components.UserList.userssaved": "Users saved!",
|
"components.UserList.userssaved": "Users saved!",
|
||||||
"components.UserList.usertype": "User Type",
|
"components.UserList.usertype": "User Type",
|
||||||
|
Reference in New Issue
Block a user