Compare commits

..

4 Commits

Author SHA1 Message Date
Gauthier
95ce4c5019 fix(proxy): apply all proxy settings to Axios 2025-06-24 10:50:22 +02:00
Ludovic Ortega
0fd03f3848 chore(helm): bump jellyseerr to 2.7.0 (#1725)
Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2025-06-20 12:39:02 +00:00
Gauthier
9cb7e1495a fix: redirect the 'Request' button to the right page (#1711)
This PR change the link of the 'Request' button of the UserDropdown for user with no MANAGE_REQUEST
or REQUEST_VIEW permissions. These users can't see the /users/:ID/requests page, so there were
redirected to the home page. This PR also removes the /profile/request page which is the same as the
/request or the /users/:ID/requests page.

fix #1588
2025-06-18 03:00:48 +05:00
Gauthier
0357d17205 feat: add force ipv4 first setting (#1719)
This PR adds a 'Force IPv4' setting in the network settings, like the one that existed before we
migrated from Fetch API to Axios.
2025-06-17 21:35:52 +02:00
19 changed files with 134 additions and 286 deletions

View File

@@ -3,8 +3,8 @@ kubeVersion: ">=1.23.0-0"
name: jellyseerr-chart
description: Jellyseerr helm chart for Kubernetes
type: application
version: 2.5.0
appVersion: "2.6.0"
version: 2.6.0
appVersion: "2.7.0"
maintainers:
- name: Jellyseerr
url: https://github.com/Fallenbagel/jellyseerr

View File

@@ -1,6 +1,6 @@
# jellyseerr-chart
![Version: 2.5.0](https://img.shields.io/badge/Version-2.5.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.6.0](https://img.shields.io/badge/AppVersion-2.6.0-informational?style=flat-square)
![Version: 2.6.0](https://img.shields.io/badge/Version-2.6.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.7.0](https://img.shields.io/badge/AppVersion-2.7.0-informational?style=flat-square)
Jellyseerr helm chart for Kubernetes

View File

@@ -36,7 +36,7 @@ describe('User List', () => {
cy.get('#email').type(testUser.emailAddress);
cy.get('#password').type(testUser.password);
cy.intercept('/api/v1/user*').as('user');
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').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*').as('user');
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
cy.get('[data-testid=modal-ok-button]').should('contain', 'Delete').click();

View File

@@ -105,6 +105,12 @@ In some places (like China), the ISP blocks not only the DNS resolution but also
You can configure Jellyseerr to use a proxy with the [HTTP(S) Proxy](/using-jellyseerr/settings/general#https-proxy) setting.
### Option 3: Force IPV4 resolution first
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
You can try to force the resolution to use IPV4 first by going to `Settings > Networking > Advanced Networking` and enabling `Force IPv4 Resolution First` setting and restarting Jellyseerr.
### Option 4: Check that your server can reach TMDB API
Make sure that your server can reach the TMDB API by running the following command:

View File

@@ -3909,14 +3909,8 @@ paths:
name: sort
schema:
type: string
enum: [created, updated, requests, displayname, usertype, role]
enum: [created, updated, requests, displayname]
default: created
- in: query
name: sortDirection
schema:
type: string
enum: [asc, desc]
default: desc
- in: query
name: q
required: false

4
pnpm-lock.yaml generated
View File

@@ -15811,7 +15811,7 @@ snapshots:
debug: 4.3.5
enhanced-resolve: 5.17.0
eslint: 8.35.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.54.0(eslint@8.35.0)(typescript@4.9.5))(eslint@8.35.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.5
@@ -15833,7 +15833,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0):
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0):
dependencies:
debug: 3.2.7(supports-color@8.1.1)
optionalDependencies:

View File

@@ -37,6 +37,8 @@ class ExternalAPI {
...options.headers,
},
});
this.axios.interceptors.request = axios.interceptors.request;
this.axios.interceptors.response = axios.interceptors.response;
if (options.rateLimit) {
this.axios = rateLimit(this.axios, {

View File

@@ -123,6 +123,8 @@ class TautulliAPI {
}${settings.urlBase ?? ''}`,
params: { apikey: settings.apiKey },
});
this.axios.interceptors.request = axios.interceptors.request;
this.axios.interceptors.response = axios.interceptors.response;
}
public async getInfo(): Promise<TautulliInfo> {

View File

@@ -28,6 +28,7 @@ import { getAppVersion } from '@server/utils/appVersion';
import createCustomProxyAgent from '@server/utils/customProxyAgent';
import restartFlag from '@server/utils/restartFlag';
import { getClientIp } from '@supercharge/request-ip';
import axios from 'axios';
import { TypeormStore } from 'connect-typeorm/out';
import cookieParser from 'cookie-parser';
import type { NextFunction, Request, Response } from 'express';
@@ -35,6 +36,8 @@ import express from 'express';
import * as OpenApiValidator from 'express-openapi-validator';
import type { Store } from 'express-session';
import session from 'express-session';
import http from 'http';
import https from 'https';
import next from 'next';
import path from 'path';
import swaggerUi from 'swagger-ui-express';
@@ -73,6 +76,11 @@ app
const settings = await getSettings().load();
restartFlag.initializeSettings(settings);
if (settings.network.forceIpv4First) {
axios.defaults.httpAgent = new http.Agent({ family: 4 });
axios.defaults.httpsAgent = new https.Agent({ family: 4 });
}
// Register HTTP proxy
if (settings.network.proxy.enabled) {
await createCustomProxyAgent(settings.network.proxy);

View File

@@ -150,6 +150,8 @@ class ImageProxy {
baseURL: baseUrl,
headers: options.headers,
});
this.axios.interceptors.request = axios.interceptors.request;
this.axios.interceptors.response = axios.interceptors.response;
if (options.rateLimitOptions) {
this.axios = rateLimit(this.axios, options.rateLimitOptions);

View File

@@ -140,6 +140,7 @@ export interface MainSettings {
export interface NetworkSettings {
csrfProtection: boolean;
forceIpv4First: boolean;
trustProxy: boolean;
proxy: ProxySettings;
}
@@ -544,6 +545,7 @@ class Settings {
},
network: {
csrfProtection: false,
forceIpv4First: false,
trustProxy: false,
proxy: {
enabled: false,

View File

@@ -42,9 +42,6 @@ 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) {
@@ -59,31 +56,28 @@ 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', sortDirection);
query = query.orderBy('user.updatedAt', 'DESC');
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"
ELSE
LOWER(user.jellyfinUsername)
END)
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.plexUsername)
LOWER(user.jellyfinUsername)
END)
ELSE
LOWER(user.username)
END`,
LOWER(user.jellyfinUsername)
END)
ELSE
LOWER(user.username)
END`,
'displayname_sort_key'
)
.orderBy('displayname_sort_key', sortDirection);
.orderBy('displayname_sort_key', 'ASC');
break;
case 'requests':
query = query
@@ -93,16 +87,10 @@ router.get('/', async (req, res, next) => {
.from(MediaRequest, 'request')
.where('request.requestedBy.id = user.id');
}, 'request_count')
.orderBy('request_count', sortDirection);
break;
case 'usertype':
query = query.orderBy('user.userType', sortDirection);
break;
case 'role':
query = query.orderBy('user.permissions', sortDirection);
.orderBy('request_count', 'DESC');
break;
default:
query = query.orderBy('user.id', sortDirection);
query = query.orderBy('user.id', 'ASC');
break;
}

View File

@@ -56,12 +56,9 @@ export default async function createCustomProxyAgent(
: undefined;
try {
const proxyUrl =
(proxySettings.useSsl ? 'https://' : 'http://') +
proxySettings.hostname +
':' +
proxySettings.port;
const proxyUrl = `${proxySettings.useSsl ? 'https' : 'http'}://${
proxySettings.hostname
}:${proxySettings.port}`;
const proxyAgent = new ProxyAgent({
uri: proxyUrl,
token,
@@ -70,10 +67,17 @@ export default async function createCustomProxyAgent(
setGlobalDispatcher(proxyAgent.compose(noProxyInterceptor));
axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl);
axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl);
axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl, {
headers: token ? { 'proxy-authorization': token } : undefined,
});
axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl, {
headers: token ? { 'proxy-authorization': token } : undefined,
});
axios.interceptors.request.use((config) => {
if (config.url && skipUrl(config.url)) {
const url = config.baseURL
? new URL(config.baseURL + (config.url || ''))
: config.url;
if (url && skipUrl(url)) {
config.httpAgent = false;
config.httpsAgent = false;
}

View File

@@ -1,6 +1,6 @@
import CachedImage from '@app/components/Common/CachedImage';
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
import { useUser } from '@app/hooks/useUser';
import { Permission, useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { Menu, Transition } from '@headlessui/react';
import {
@@ -36,7 +36,7 @@ ForwardedLink.displayName = 'ForwardedLink';
const UserDropdown = () => {
const intl = useIntl();
const { user, revalidate } = useUser();
const { user, revalidate, hasPermission } = useUser();
const logout = async () => {
const response = await axios.post('/api/v1/auth/logout');
@@ -118,7 +118,14 @@ const UserDropdown = () => {
<Menu.Item>
{({ active }) => (
<ForwardedLink
href={`/users/${user?.id}/requests?filter=all`}
href={
hasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' }
)
? `/users/${user?.id}/requests?filter=all`
: '/requests'
}
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
active
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'

View File

@@ -42,6 +42,9 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
networkDisclaimer:
'Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.',
docs: 'documentation',
forceIpv4First: 'Force IPv4 Resolution First',
forceIpv4FirstTip:
'Force Jellyseerr to resolve IPv4 addresses first instead of IPv6',
});
const SettingsNetwork = () => {
@@ -86,6 +89,7 @@ const SettingsNetwork = () => {
<Formik
initialValues={{
csrfProtection: data?.csrfProtection,
forceIpv4First: data?.forceIpv4First,
trustProxy: data?.trustProxy,
proxyEnabled: data?.proxy?.enabled,
proxyHostname: data?.proxy?.hostname,
@@ -102,6 +106,7 @@ const SettingsNetwork = () => {
try {
await axios.post('/api/v1/settings/network', {
csrfProtection: values.csrfProtection,
forceIpv4First: values.forceIpv4First,
trustProxy: values.trustProxy,
proxy: {
enabled: values.proxyEnabled,
@@ -193,6 +198,29 @@ const SettingsNetwork = () => {
</Tooltip>
</div>
</div>
<div className="form-row">
<label htmlFor="forceIpv4First" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.forceIpv4First)}
</span>
<SettingsBadge badgeType="advanced" className="mr-2" />
<SettingsBadge badgeType="restartRequired" />
<SettingsBadge badgeType="experimental" />
<span className="label-tip">
{intl.formatMessage(messages.forceIpv4FirstTip)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="forceIpv4First"
name="forceIpv4First"
onChange={() => {
setFieldValue('forceIpv4First', !values.forceIpv4First);
}}
/>
</div>
</div>
<div className="form-row">
<label htmlFor="proxyEnabled" className="checkbox-label">
<span className="mr-2">

View File

@@ -19,11 +19,8 @@ import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import {
BarsArrowDownIcon,
BarsArrowUpIcon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
ChevronUpIcon,
InboxArrowDownIcon,
PencilIcon,
UserPlusIcon,
@@ -80,27 +77,14 @@ const messages = defineMessages('components.UserList', {
autogeneratepasswordTip: 'Email a server-generated password to the user',
validationUsername: 'You must provide an username',
validationEmail: 'Email required',
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',
sortCreated: 'Join Date',
sortDisplayName: 'Display Name',
sortRequests: 'Request Count',
localLoginDisabled:
'The <strong>Enable Local Sign-In</strong> setting is currently disabled.',
});
type Sort =
| 'created'
| 'updated'
| 'requests'
| 'displayname'
| 'usertype'
| 'role';
type SortDirection = 'asc' | 'desc';
type Sort = 'created' | 'updated' | 'requests' | 'displayname';
const UserList = () => {
const intl = useIntl();
@@ -110,12 +94,10 @@ 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,
@@ -124,59 +106,9 @@ const UserList = () => {
} = useSWR<UserResultsResponse>(
`/api/v1/user?take=${currentPageSize}&skip=${
pageIndex * currentPageSize
}&sort=created&sortDirection=desc`
}&sort=${currentSort}`
);
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<{
@@ -201,9 +133,6 @@ const UserList = () => {
setCurrentSort(filterSettings.currentSort);
setCurrentPageSize(filterSettings.currentPageSize);
if (filterSettings.sortDirection) {
setSortDirection(filterSettings.sortDirection);
}
}
}, []);
@@ -213,74 +142,9 @@ const UserList = () => {
JSON.stringify({
currentSort,
currentPageSize,
sortDirection,
})
);
}, [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>
);
};
}, [currentSort, currentPageSize]);
const isUserPermsEditable = (userId: number) =>
userId !== 1 && userId !== currentUser?.id;
@@ -677,47 +541,28 @@ const UserList = () => {
</span>
</Button>
</div>
<div className="mb-2 flex flex-grow lg:mb-0 lg:flex-grow-0">
<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>
<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>
<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="displayname">
{intl.formatMessage(messages.username)}
<option value="created">
{intl.formatMessage(messages.sortCreated)}
</option>
<option value="requests">
{intl.formatMessage(messages.totalrequests)}
{intl.formatMessage(messages.sortRequests)}
</option>
<option value="usertype">
{intl.formatMessage(messages.accounttype)}
</option>
<option value="role">{intl.formatMessage(messages.role)}</option>
<option value="created">
{intl.formatMessage(messages.created)}
<option value="displayname">
{intl.formatMessage(messages.sortDisplayName)}
</option>
</select>
</div>
@@ -739,46 +584,11 @@ const UserList = () => {
/>
)}
</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>{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>
<Table.TH className="text-right">
{(data.results ?? []).length > 1 && (
<Button
@@ -794,7 +604,7 @@ const UserList = () => {
</tr>
</thead>
<Table.TBody>
{localUsers.map((user) => (
{data?.results.map((user) => (
<tr key={`user-list-${user.id}`} data-testid="user-list-row">
<Table.TD>
{isUserPermsEditable(user.id) && (

View File

@@ -160,9 +160,12 @@ const UserProfile = () => {
<dd className="mt-1 text-3xl font-semibold text-white">
<Link
href={
user.id === currentUser?.id
? '/profile/requests?filter=all'
: `/users/${user?.id}/requests?filter=all`
currentHasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' }
)
? `/users/${user?.id}/requests?filter=all`
: '/requests'
}
>
{intl.formatNumber(user.requestCount)}
@@ -293,9 +296,12 @@ const UserProfile = () => {
<div className="slider-header">
<Link
href={
user.id === currentUser?.id
? '/profile/requests?filter=all'
: `/users/${user?.id}/requests?filter=all`
currentHasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' }
)
? `/users/${user?.id}/requests?filter=all`
: '/requests'
}
className="slider-title"
>

View File

@@ -978,10 +978,13 @@
"components.Settings.SettingsMain.validationUrl": "You must provide a valid URL",
"components.Settings.SettingsMain.validationUrlTrailingSlash": "URL must not end in a trailing slash",
"components.Settings.SettingsMain.youtubeUrl": "YouTube URL",
"components.Settings.SettingsMain.youtubeUrlTip": "Base URL for YouTube videos if a self-hosted YouTube instance is used.",
"components.Settings.SettingsNetwork.csrfProtection": "Enable CSRF Protection",
"components.Settings.SettingsNetwork.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
"components.Settings.SettingsNetwork.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
"components.Settings.SettingsNetwork.docs": "documentation",
"components.Settings.SettingsNetwork.forceIpv4First": "Force IPv4 Resolution First",
"components.Settings.SettingsNetwork.forceIpv4FirstTip": "Force Jellyseerr to resolve IPv4 addresses first instead of IPv6",
"components.Settings.SettingsNetwork.network": "Network",
"components.Settings.SettingsNetwork.networkDisclaimer": "Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.",
"components.Settings.SettingsNetwork.networksettings": "Network Settings",
@@ -1213,7 +1216,7 @@
"components.Setup.librarieserror": "Validation failed. Please toggle the libraries again to continue.",
"components.Setup.servertype": "Choose Server Type",
"components.Setup.setup": "Setup",
"components.Setup.signin": "Sign in to your account",
"components.Setup.signin": "Sign In",
"components.Setup.signinMessage": "Get started by signing in",
"components.Setup.signinWithEmby": "Enter your Emby details",
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
@@ -1282,7 +1285,6 @@
"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",
@@ -1292,7 +1294,6 @@
"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!",
@@ -1314,13 +1315,9 @@
"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.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.sortCreated": "Join Date",
"components.UserList.sortDisplayName": "Display Name",
"components.UserList.sortRequests": "Request Count",
"components.UserList.totalrequests": "Requests",
"components.UserList.user": "User",
"components.UserList.usercreatedfailed": "Something went wrong while creating the user.",

View File

@@ -1,8 +0,0 @@
import RequestList from '@app/components/RequestList';
import type { NextPage } from 'next';
const UserRequestsPage: NextPage = () => {
return <RequestList />;
};
export default UserRequestsPage;