mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
Compare commits
7 Commits
preview-fi
...
preview-so
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a7529dc07 | ||
|
|
cbee8fd843 | ||
|
|
b9435427dc | ||
|
|
8ceec0f9c4 | ||
|
|
5a1040bb61 | ||
|
|
a97a3f3512 | ||
|
|
1dbacec4f9 |
@@ -36,7 +36,7 @@ describe('User List', () => {
|
||||
cy.get('#email').type(testUser.emailAddress);
|
||||
cy.get('#password').type(testUser.password);
|
||||
|
||||
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
|
||||
cy.intercept('/api/v1/user*').as('user');
|
||||
|
||||
cy.get('[data-testid=modal-ok-button]').click();
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('User List', () => {
|
||||
|
||||
cy.get('[data-testid=modal-title]').should('contain', `Delete User`);
|
||||
|
||||
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
|
||||
cy.intercept('/api/v1/user*').as('user');
|
||||
|
||||
cy.get('[data-testid=modal-ok-button]').should('contain', 'Delete').click();
|
||||
|
||||
|
||||
@@ -3909,8 +3909,14 @@ paths:
|
||||
name: sort
|
||||
schema:
|
||||
type: string
|
||||
enum: [created, updated, requests, displayname]
|
||||
enum: [created, updated, requests, displayname, usertype, role]
|
||||
default: created
|
||||
- in: query
|
||||
name: sortDirection
|
||||
schema:
|
||||
type: string
|
||||
enum: [asc, desc]
|
||||
default: desc
|
||||
- in: query
|
||||
name: q
|
||||
required: false
|
||||
|
||||
@@ -42,6 +42,9 @@ router.get('/', async (req, res, next) => {
|
||||
: Math.max(10, includeIds.length);
|
||||
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
||||
const q = req.query.q ? req.query.q.toString().toLowerCase() : '';
|
||||
const sortDirection =
|
||||
(req.query.sortDirection as string) === 'asc' ? 'ASC' : 'DESC';
|
||||
|
||||
let query = getRepository(User).createQueryBuilder('user');
|
||||
|
||||
if (q) {
|
||||
@@ -56,28 +59,31 @@ router.get('/', async (req, res, next) => {
|
||||
}
|
||||
|
||||
switch (req.query.sort) {
|
||||
case 'created':
|
||||
query = query.orderBy('user.createdAt', sortDirection);
|
||||
break;
|
||||
case 'updated':
|
||||
query = query.orderBy('user.updatedAt', 'DESC');
|
||||
query = query.orderBy('user.updatedAt', sortDirection);
|
||||
break;
|
||||
case 'displayname':
|
||||
query = query
|
||||
.addSelect(
|
||||
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
||||
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
||||
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
||||
"user"."email"
|
||||
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
||||
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
||||
"user"."email"
|
||||
ELSE
|
||||
LOWER(user.jellyfinUsername)
|
||||
END)
|
||||
ELSE
|
||||
LOWER(user.jellyfinUsername)
|
||||
LOWER(user.plexUsername)
|
||||
END)
|
||||
ELSE
|
||||
LOWER(user.jellyfinUsername)
|
||||
END)
|
||||
ELSE
|
||||
LOWER(user.username)
|
||||
END`,
|
||||
LOWER(user.username)
|
||||
END`,
|
||||
'displayname_sort_key'
|
||||
)
|
||||
.orderBy('displayname_sort_key', 'ASC');
|
||||
.orderBy('displayname_sort_key', sortDirection);
|
||||
break;
|
||||
case 'requests':
|
||||
query = query
|
||||
@@ -87,10 +93,16 @@ router.get('/', async (req, res, next) => {
|
||||
.from(MediaRequest, 'request')
|
||||
.where('request.requestedBy.id = user.id');
|
||||
}, 'request_count')
|
||||
.orderBy('request_count', 'DESC');
|
||||
.orderBy('request_count', sortDirection);
|
||||
break;
|
||||
case 'usertype':
|
||||
query = query.orderBy('user.userType', sortDirection);
|
||||
break;
|
||||
case 'role':
|
||||
query = query.orderBy('user.permissions', sortDirection);
|
||||
break;
|
||||
default:
|
||||
query = query.orderBy('user.id', 'ASC');
|
||||
query = query.orderBy('user.id', sortDirection);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,11 @@ import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import {
|
||||
BarsArrowDownIcon,
|
||||
BarsArrowUpIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronUpIcon,
|
||||
InboxArrowDownIcon,
|
||||
PencilIcon,
|
||||
UserPlusIcon,
|
||||
@@ -77,14 +80,27 @@ const messages = defineMessages('components.UserList', {
|
||||
autogeneratepasswordTip: 'Email a server-generated password to the user',
|
||||
validationUsername: 'You must provide an username',
|
||||
validationEmail: 'Email required',
|
||||
sortCreated: 'Join Date',
|
||||
sortDisplayName: 'Display Name',
|
||||
sortRequests: 'Request Count',
|
||||
sortBy: 'Sort by {field}',
|
||||
sortByUser: 'Sort by username',
|
||||
sortByRequests: 'Sort by number of requests',
|
||||
sortByType: 'Sort by account type',
|
||||
sortByRole: 'Sort by user role',
|
||||
sortByJoined: 'Sort by join date',
|
||||
toggleSortDirection: 'Click again to sort {direction}',
|
||||
ascending: 'ascending',
|
||||
descending: 'descending',
|
||||
localLoginDisabled:
|
||||
'The <strong>Enable Local Sign-In</strong> setting is currently disabled.',
|
||||
});
|
||||
|
||||
type Sort = 'created' | 'updated' | 'requests' | 'displayname';
|
||||
type Sort =
|
||||
| 'created'
|
||||
| 'updated'
|
||||
| 'requests'
|
||||
| 'displayname'
|
||||
| 'usertype'
|
||||
| 'role';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
const UserList = () => {
|
||||
const intl = useIntl();
|
||||
@@ -94,10 +110,12 @@ const UserList = () => {
|
||||
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
|
||||
const [currentSort, setCurrentSort] = useState<Sort>('displayname');
|
||||
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
||||
const [localUsers, setLocalUsers] = useState<User[]>([]);
|
||||
|
||||
const page = router.query.page ? Number(router.query.page) : 1;
|
||||
const pageIndex = page - 1;
|
||||
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -106,9 +124,59 @@ const UserList = () => {
|
||||
} = useSWR<UserResultsResponse>(
|
||||
`/api/v1/user?take=${currentPageSize}&skip=${
|
||||
pageIndex * currentPageSize
|
||||
}&sort=${currentSort}`
|
||||
}&sort=created&sortDirection=desc`
|
||||
);
|
||||
|
||||
const sortUsers = (
|
||||
users: User[],
|
||||
sortKey: Sort,
|
||||
direction: SortDirection
|
||||
) => {
|
||||
return [...users].sort((a, b) => {
|
||||
let comparison = 0;
|
||||
switch (sortKey) {
|
||||
case 'displayname':
|
||||
comparison = a.displayName.localeCompare(b.displayName);
|
||||
break;
|
||||
case 'requests':
|
||||
comparison = (a.requestCount ?? 0) - (b.requestCount ?? 0);
|
||||
break;
|
||||
case 'usertype':
|
||||
comparison = a.userType - b.userType;
|
||||
break;
|
||||
case 'role':
|
||||
comparison = a.permissions - b.permissions;
|
||||
break;
|
||||
case 'created':
|
||||
comparison =
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
break;
|
||||
default:
|
||||
comparison = 0;
|
||||
}
|
||||
return direction === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.results) {
|
||||
setLocalUsers(sortUsers(data.results, currentSort, sortDirection));
|
||||
}
|
||||
}, [data, currentSort, sortDirection]);
|
||||
|
||||
const handleSortChange = (sortKey: Sort) => {
|
||||
const newSortDirection =
|
||||
currentSort === sortKey
|
||||
? sortDirection === 'asc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
: 'desc';
|
||||
|
||||
setCurrentSort(sortKey);
|
||||
setSortDirection(newSortDirection);
|
||||
setLocalUsers(sortUsers(localUsers, sortKey, newSortDirection));
|
||||
};
|
||||
|
||||
const [isDeleting, setDeleting] = useState(false);
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState<{
|
||||
@@ -133,6 +201,9 @@ const UserList = () => {
|
||||
|
||||
setCurrentSort(filterSettings.currentSort);
|
||||
setCurrentPageSize(filterSettings.currentPageSize);
|
||||
if (filterSettings.sortDirection) {
|
||||
setSortDirection(filterSettings.sortDirection);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -142,9 +213,74 @@ const UserList = () => {
|
||||
JSON.stringify({
|
||||
currentSort,
|
||||
currentPageSize,
|
||||
sortDirection,
|
||||
})
|
||||
);
|
||||
}, [currentSort, currentPageSize]);
|
||||
}, [currentSort, currentPageSize, sortDirection]);
|
||||
|
||||
const SortableColumnHeader = ({
|
||||
sortKey,
|
||||
currentSort,
|
||||
sortDirection,
|
||||
onSortChange,
|
||||
children,
|
||||
}: {
|
||||
sortKey: Sort;
|
||||
currentSort: Sort;
|
||||
sortDirection: SortDirection;
|
||||
onSortChange: (sortKey: Sort) => void;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const getTooltip = () => {
|
||||
if (currentSort === sortKey) {
|
||||
return intl.formatMessage(messages.toggleSortDirection, {
|
||||
direction:
|
||||
sortDirection === 'asc'
|
||||
? intl.formatMessage(messages.descending)
|
||||
: intl.formatMessage(messages.ascending),
|
||||
});
|
||||
}
|
||||
|
||||
switch (sortKey) {
|
||||
case 'displayname':
|
||||
return intl.formatMessage(messages.sortByUser);
|
||||
case 'requests':
|
||||
return intl.formatMessage(messages.sortByRequests);
|
||||
case 'usertype':
|
||||
return intl.formatMessage(messages.sortByType);
|
||||
case 'role':
|
||||
return intl.formatMessage(messages.sortByRole);
|
||||
case 'created':
|
||||
return intl.formatMessage(messages.sortByJoined);
|
||||
default:
|
||||
return intl.formatMessage(messages.sortBy, { field: sortKey });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Table.TH
|
||||
className="cursor-pointer hover:bg-gray-700"
|
||||
onClick={() => onSortChange(sortKey)}
|
||||
data-testid={`column-header-${sortKey}`}
|
||||
title={getTooltip()}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>{children}</span>
|
||||
{currentSort === sortKey && (
|
||||
<span className="ml-1">
|
||||
{sortDirection === 'asc' ? (
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Table.TH>
|
||||
);
|
||||
};
|
||||
|
||||
const isUserPermsEditable = (userId: number) =>
|
||||
userId !== 1 && userId !== currentUser?.id;
|
||||
@@ -541,28 +677,47 @@ const UserList = () => {
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 flex flex-grow lg:mb-0 lg:flex-grow-0">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
|
||||
<BarsArrowDownIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100"
|
||||
onClick={() =>
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
||||
}
|
||||
title={
|
||||
sortDirection === 'asc'
|
||||
? intl.formatMessage(messages.descending)
|
||||
: intl.formatMessage(messages.ascending)
|
||||
}
|
||||
>
|
||||
{sortDirection === 'asc' ? (
|
||||
<BarsArrowUpIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<BarsArrowDownIcon className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
<select
|
||||
id="sort"
|
||||
name="sort"
|
||||
onChange={(e) => {
|
||||
setCurrentSort(e.target.value as Sort);
|
||||
router.push(router.pathname);
|
||||
}}
|
||||
value={currentSort}
|
||||
className="rounded-r-only"
|
||||
>
|
||||
<option value="created">
|
||||
{intl.formatMessage(messages.sortCreated)}
|
||||
<option value="displayname">
|
||||
{intl.formatMessage(messages.username)}
|
||||
</option>
|
||||
<option value="requests">
|
||||
{intl.formatMessage(messages.sortRequests)}
|
||||
{intl.formatMessage(messages.totalrequests)}
|
||||
</option>
|
||||
<option value="displayname">
|
||||
{intl.formatMessage(messages.sortDisplayName)}
|
||||
<option value="usertype">
|
||||
{intl.formatMessage(messages.accounttype)}
|
||||
</option>
|
||||
<option value="role">{intl.formatMessage(messages.role)}</option>
|
||||
<option value="created">
|
||||
{intl.formatMessage(messages.created)}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -584,11 +739,46 @@ const UserList = () => {
|
||||
/>
|
||||
)}
|
||||
</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.user)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.totalrequests)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.accounttype)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.role)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.created)}</Table.TH>
|
||||
<SortableColumnHeader
|
||||
sortKey="displayname"
|
||||
currentSort={currentSort}
|
||||
sortDirection={sortDirection}
|
||||
onSortChange={handleSortChange}
|
||||
>
|
||||
{intl.formatMessage(messages.user)}
|
||||
</SortableColumnHeader>
|
||||
<SortableColumnHeader
|
||||
sortKey="requests"
|
||||
currentSort={currentSort}
|
||||
sortDirection={sortDirection}
|
||||
onSortChange={handleSortChange}
|
||||
>
|
||||
{intl.formatMessage(messages.totalrequests)}
|
||||
</SortableColumnHeader>
|
||||
<SortableColumnHeader
|
||||
sortKey="usertype"
|
||||
currentSort={currentSort}
|
||||
sortDirection={sortDirection}
|
||||
onSortChange={handleSortChange}
|
||||
>
|
||||
{intl.formatMessage(messages.accounttype)}
|
||||
</SortableColumnHeader>
|
||||
<SortableColumnHeader
|
||||
sortKey="role"
|
||||
currentSort={currentSort}
|
||||
sortDirection={sortDirection}
|
||||
onSortChange={handleSortChange}
|
||||
>
|
||||
{intl.formatMessage(messages.role)}
|
||||
</SortableColumnHeader>
|
||||
<SortableColumnHeader
|
||||
sortKey="created"
|
||||
currentSort={currentSort}
|
||||
sortDirection={sortDirection}
|
||||
onSortChange={handleSortChange}
|
||||
>
|
||||
{intl.formatMessage(messages.created)}
|
||||
</SortableColumnHeader>
|
||||
<Table.TH className="text-right">
|
||||
{(data.results ?? []).length > 1 && (
|
||||
<Button
|
||||
@@ -604,7 +794,7 @@ const UserList = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
{data?.results.map((user) => (
|
||||
{localUsers.map((user) => (
|
||||
<tr key={`user-list-${user.id}`} data-testid="user-list-row">
|
||||
<Table.TD>
|
||||
{isUserPermsEditable(user.id) && (
|
||||
|
||||
@@ -1282,6 +1282,7 @@
|
||||
"components.TvDetails.watchtrailer": "Watch Trailer",
|
||||
"components.UserList.accounttype": "Type",
|
||||
"components.UserList.admin": "Admin",
|
||||
"components.UserList.ascending": "ascending",
|
||||
"components.UserList.autogeneratepassword": "Automatically Generate Password",
|
||||
"components.UserList.autogeneratepasswordTip": "Email a server-generated password to the user",
|
||||
"components.UserList.bulkedit": "Bulk Edit",
|
||||
@@ -1291,6 +1292,7 @@
|
||||
"components.UserList.creating": "Creating…",
|
||||
"components.UserList.deleteconfirm": "Are you sure you want to delete this user? All of their request data will be permanently removed.",
|
||||
"components.UserList.deleteuser": "Delete User",
|
||||
"components.UserList.descending": "descending",
|
||||
"components.UserList.edituser": "Edit User Permissions",
|
||||
"components.UserList.email": "Email Address",
|
||||
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!",
|
||||
@@ -1312,9 +1314,13 @@
|
||||
"components.UserList.passwordinfodescription": "Configure an application URL and enable email notifications to allow automatic password generation.",
|
||||
"components.UserList.plexuser": "Plex User",
|
||||
"components.UserList.role": "Role",
|
||||
"components.UserList.sortCreated": "Join Date",
|
||||
"components.UserList.sortDisplayName": "Display Name",
|
||||
"components.UserList.sortRequests": "Request Count",
|
||||
"components.UserList.sortBy": "Sort by {field}",
|
||||
"components.UserList.sortByJoined": "Sort by join date",
|
||||
"components.UserList.sortByRequests": "Sort by number of requests",
|
||||
"components.UserList.sortByRole": "Sort by user role",
|
||||
"components.UserList.sortByType": "Sort by account type",
|
||||
"components.UserList.sortByUser": "Sort by username",
|
||||
"components.UserList.toggleSortDirection": "Click again to sort {direction}",
|
||||
"components.UserList.totalrequests": "Requests",
|
||||
"components.UserList.user": "User",
|
||||
"components.UserList.usercreatedfailed": "Something went wrong while creating the user.",
|
||||
|
||||
Reference in New Issue
Block a user