Compare commits
7 Commits
preview-av
...
preview-so
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a7529dc07 | ||
|
|
cbee8fd843 | ||
|
|
b9435427dc | ||
|
|
8ceec0f9c4 | ||
|
|
5a1040bb61 | ||
|
|
a97a3f3512 | ||
|
|
1dbacec4f9 |
@@ -3,8 +3,8 @@ kubeVersion: ">=1.23.0-0"
|
|||||||
name: jellyseerr-chart
|
name: jellyseerr-chart
|
||||||
description: Jellyseerr helm chart for Kubernetes
|
description: Jellyseerr helm chart for Kubernetes
|
||||||
type: application
|
type: application
|
||||||
version: 2.6.0
|
version: 2.5.0
|
||||||
appVersion: "2.7.0"
|
appVersion: "2.6.0"
|
||||||
maintainers:
|
maintainers:
|
||||||
- name: Jellyseerr
|
- name: Jellyseerr
|
||||||
url: https://github.com/Fallenbagel/jellyseerr
|
url: https://github.com/Fallenbagel/jellyseerr
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# jellyseerr-chart
|
# jellyseerr-chart
|
||||||
|
|
||||||
  
|
  
|
||||||
|
|
||||||
Jellyseerr helm chart for Kubernetes
|
Jellyseerr helm chart for Kubernetes
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ describe('User List', () => {
|
|||||||
cy.get('#email').type(testUser.emailAddress);
|
cy.get('#email').type(testUser.emailAddress);
|
||||||
cy.get('#password').type(testUser.password);
|
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();
|
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.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();
|
cy.get('[data-testid=modal-ok-button]').should('contain', 'Delete').click();
|
||||||
|
|
||||||
|
|||||||
@@ -105,12 +105,6 @@ 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.
|
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
|
### 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:
|
Make sure that your server can reach the TMDB API by running the following command:
|
||||||
|
|||||||
@@ -3909,8 +3909,14 @@ paths:
|
|||||||
name: sort
|
name: sort
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: [created, updated, requests, displayname]
|
enum: [created, updated, requests, displayname, usertype, role]
|
||||||
default: created
|
default: created
|
||||||
|
- in: query
|
||||||
|
name: sortDirection
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [asc, desc]
|
||||||
|
default: desc
|
||||||
- in: query
|
- in: query
|
||||||
name: q
|
name: q
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
@@ -15811,7 +15811,7 @@ snapshots:
|
|||||||
debug: 4.3.5
|
debug: 4.3.5
|
||||||
enhanced-resolve: 5.17.0
|
enhanced-resolve: 5.17.0
|
||||||
eslint: 8.35.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(@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-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-plugin-import: 2.29.1(@typescript-eslint/parser@5.54.0(eslint@8.35.0)(typescript@4.9.5))(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
|
fast-glob: 3.3.2
|
||||||
get-tsconfig: 4.7.5
|
get-tsconfig: 4.7.5
|
||||||
@@ -15833,7 +15833,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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(@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-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):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@8.1.1)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ class ExternalAPI {
|
|||||||
...options.headers,
|
...options.headers,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.axios.interceptors.request = axios.interceptors.request;
|
|
||||||
this.axios.interceptors.response = axios.interceptors.response;
|
|
||||||
|
|
||||||
if (options.rateLimit) {
|
if (options.rateLimit) {
|
||||||
this.axios = rateLimit(this.axios, {
|
this.axios = rateLimit(this.axios, {
|
||||||
|
|||||||
@@ -123,8 +123,6 @@ class TautulliAPI {
|
|||||||
}${settings.urlBase ?? ''}`,
|
}${settings.urlBase ?? ''}`,
|
||||||
params: { apikey: settings.apiKey },
|
params: { apikey: settings.apiKey },
|
||||||
});
|
});
|
||||||
this.axios.interceptors.request = axios.interceptors.request;
|
|
||||||
this.axios.interceptors.response = axios.interceptors.response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getInfo(): Promise<TautulliInfo> {
|
public async getInfo(): Promise<TautulliInfo> {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { getAppVersion } from '@server/utils/appVersion';
|
|||||||
import createCustomProxyAgent from '@server/utils/customProxyAgent';
|
import createCustomProxyAgent from '@server/utils/customProxyAgent';
|
||||||
import restartFlag from '@server/utils/restartFlag';
|
import restartFlag from '@server/utils/restartFlag';
|
||||||
import { getClientIp } from '@supercharge/request-ip';
|
import { getClientIp } from '@supercharge/request-ip';
|
||||||
import axios from 'axios';
|
|
||||||
import { TypeormStore } from 'connect-typeorm/out';
|
import { TypeormStore } from 'connect-typeorm/out';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import type { NextFunction, Request, Response } from 'express';
|
import type { NextFunction, Request, Response } from 'express';
|
||||||
@@ -36,8 +35,6 @@ import express from 'express';
|
|||||||
import * as OpenApiValidator from 'express-openapi-validator';
|
import * as OpenApiValidator from 'express-openapi-validator';
|
||||||
import type { Store } from 'express-session';
|
import type { Store } from 'express-session';
|
||||||
import session from 'express-session';
|
import session from 'express-session';
|
||||||
import http from 'http';
|
|
||||||
import https from 'https';
|
|
||||||
import next from 'next';
|
import next from 'next';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import swaggerUi from 'swagger-ui-express';
|
import swaggerUi from 'swagger-ui-express';
|
||||||
@@ -76,11 +73,6 @@ app
|
|||||||
const settings = await getSettings().load();
|
const settings = await getSettings().load();
|
||||||
restartFlag.initializeSettings(settings);
|
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
|
// Register HTTP proxy
|
||||||
if (settings.network.proxy.enabled) {
|
if (settings.network.proxy.enabled) {
|
||||||
await createCustomProxyAgent(settings.network.proxy);
|
await createCustomProxyAgent(settings.network.proxy);
|
||||||
|
|||||||
@@ -150,8 +150,6 @@ class ImageProxy {
|
|||||||
baseURL: baseUrl,
|
baseURL: baseUrl,
|
||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
});
|
});
|
||||||
this.axios.interceptors.request = axios.interceptors.request;
|
|
||||||
this.axios.interceptors.response = axios.interceptors.response;
|
|
||||||
|
|
||||||
if (options.rateLimitOptions) {
|
if (options.rateLimitOptions) {
|
||||||
this.axios = rateLimit(this.axios, options.rateLimitOptions);
|
this.axios = rateLimit(this.axios, options.rateLimitOptions);
|
||||||
|
|||||||
@@ -140,7 +140,6 @@ export interface MainSettings {
|
|||||||
|
|
||||||
export interface NetworkSettings {
|
export interface NetworkSettings {
|
||||||
csrfProtection: boolean;
|
csrfProtection: boolean;
|
||||||
forceIpv4First: boolean;
|
|
||||||
trustProxy: boolean;
|
trustProxy: boolean;
|
||||||
proxy: ProxySettings;
|
proxy: ProxySettings;
|
||||||
}
|
}
|
||||||
@@ -545,7 +544,6 @@ class Settings {
|
|||||||
},
|
},
|
||||||
network: {
|
network: {
|
||||||
csrfProtection: false,
|
csrfProtection: false,
|
||||||
forceIpv4First: false,
|
|
||||||
trustProxy: false,
|
trustProxy: false,
|
||||||
proxy: {
|
proxy: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ router.get('/', async (req, res, next) => {
|
|||||||
: Math.max(10, includeIds.length);
|
: Math.max(10, includeIds.length);
|
||||||
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
||||||
const q = req.query.q ? req.query.q.toString().toLowerCase() : '';
|
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');
|
let query = getRepository(User).createQueryBuilder('user');
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
@@ -56,28 +59,31 @@ router.get('/', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (req.query.sort) {
|
switch (req.query.sort) {
|
||||||
|
case 'created':
|
||||||
|
query = query.orderBy('user.createdAt', sortDirection);
|
||||||
|
break;
|
||||||
case 'updated':
|
case 'updated':
|
||||||
query = query.orderBy('user.updatedAt', 'DESC');
|
query = query.orderBy('user.updatedAt', sortDirection);
|
||||||
break;
|
break;
|
||||||
case 'displayname':
|
case 'displayname':
|
||||||
query = query
|
query = query
|
||||||
.addSelect(
|
.addSelect(
|
||||||
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
||||||
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
||||||
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
||||||
"user"."email"
|
"user"."email"
|
||||||
|
ELSE
|
||||||
|
LOWER(user.jellyfinUsername)
|
||||||
|
END)
|
||||||
ELSE
|
ELSE
|
||||||
LOWER(user.jellyfinUsername)
|
LOWER(user.plexUsername)
|
||||||
END)
|
END)
|
||||||
ELSE
|
ELSE
|
||||||
LOWER(user.jellyfinUsername)
|
LOWER(user.username)
|
||||||
END)
|
END`,
|
||||||
ELSE
|
|
||||||
LOWER(user.username)
|
|
||||||
END`,
|
|
||||||
'displayname_sort_key'
|
'displayname_sort_key'
|
||||||
)
|
)
|
||||||
.orderBy('displayname_sort_key', 'ASC');
|
.orderBy('displayname_sort_key', sortDirection);
|
||||||
break;
|
break;
|
||||||
case 'requests':
|
case 'requests':
|
||||||
query = query
|
query = query
|
||||||
@@ -87,10 +93,16 @@ router.get('/', async (req, res, next) => {
|
|||||||
.from(MediaRequest, 'request')
|
.from(MediaRequest, 'request')
|
||||||
.where('request.requestedBy.id = user.id');
|
.where('request.requestedBy.id = user.id');
|
||||||
}, 'request_count')
|
}, '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;
|
break;
|
||||||
default:
|
default:
|
||||||
query = query.orderBy('user.id', 'ASC');
|
query = query.orderBy('user.id', sortDirection);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,9 +56,12 @@ export default async function createCustomProxyAgent(
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proxyUrl = `${proxySettings.useSsl ? 'https' : 'http'}://${
|
const proxyUrl =
|
||||||
proxySettings.hostname
|
(proxySettings.useSsl ? 'https://' : 'http://') +
|
||||||
}:${proxySettings.port}`;
|
proxySettings.hostname +
|
||||||
|
':' +
|
||||||
|
proxySettings.port;
|
||||||
|
|
||||||
const proxyAgent = new ProxyAgent({
|
const proxyAgent = new ProxyAgent({
|
||||||
uri: proxyUrl,
|
uri: proxyUrl,
|
||||||
token,
|
token,
|
||||||
@@ -67,17 +70,10 @@ export default async function createCustomProxyAgent(
|
|||||||
|
|
||||||
setGlobalDispatcher(proxyAgent.compose(noProxyInterceptor));
|
setGlobalDispatcher(proxyAgent.compose(noProxyInterceptor));
|
||||||
|
|
||||||
axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl, {
|
axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl);
|
||||||
headers: token ? { 'proxy-authorization': token } : undefined,
|
axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl);
|
||||||
});
|
|
||||||
axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl, {
|
|
||||||
headers: token ? { 'proxy-authorization': token } : undefined,
|
|
||||||
});
|
|
||||||
axios.interceptors.request.use((config) => {
|
axios.interceptors.request.use((config) => {
|
||||||
const url = config.baseURL
|
if (config.url && skipUrl(config.url)) {
|
||||||
? new URL(config.baseURL + (config.url || ''))
|
|
||||||
: config.url;
|
|
||||||
if (url && skipUrl(url)) {
|
|
||||||
config.httpAgent = false;
|
config.httpAgent = false;
|
||||||
config.httpsAgent = false;
|
config.httpsAgent = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import CachedImage from '@app/components/Common/CachedImage';
|
import CachedImage from '@app/components/Common/CachedImage';
|
||||||
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
|
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
|
||||||
import { Permission, useUser } from '@app/hooks/useUser';
|
import { useUser } from '@app/hooks/useUser';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { Menu, Transition } from '@headlessui/react';
|
import { Menu, Transition } from '@headlessui/react';
|
||||||
import {
|
import {
|
||||||
@@ -36,7 +36,7 @@ ForwardedLink.displayName = 'ForwardedLink';
|
|||||||
|
|
||||||
const UserDropdown = () => {
|
const UserDropdown = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { user, revalidate, hasPermission } = useUser();
|
const { user, revalidate } = useUser();
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
const response = await axios.post('/api/v1/auth/logout');
|
const response = await axios.post('/api/v1/auth/logout');
|
||||||
@@ -118,14 +118,7 @@ const UserDropdown = () => {
|
|||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<ForwardedLink
|
<ForwardedLink
|
||||||
href={
|
href={`/users/${user?.id}/requests?filter=all`}
|
||||||
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 ${
|
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
|
||||||
active
|
active
|
||||||
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
|
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Badge from '@app/components/Common/Badge';
|
import Badge from '@app/components/Common/Badge';
|
||||||
import Button from '@app/components/Common/Button';
|
import Button from '@app/components/Common/Button';
|
||||||
import CachedImage from '@app/components/Common/CachedImage';
|
|
||||||
import Tooltip from '@app/components/Common/Tooltip';
|
import Tooltip from '@app/components/Common/Tooltip';
|
||||||
import RequestModal from '@app/components/RequestModal';
|
import RequestModal from '@app/components/RequestModal';
|
||||||
import useRequestOverride from '@app/hooks/useRequestOverride';
|
import useRequestOverride from '@app/hooks/useRequestOverride';
|
||||||
@@ -96,58 +95,36 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="mr-6 min-w-0 flex-1 flex-col items-center text-sm leading-5">
|
<div className="mr-6 min-w-0 flex-1 flex-col items-center text-sm leading-5">
|
||||||
<div className="white mb-1 flex flex-nowrap">
|
<div className="white mb-1 flex flex-nowrap">
|
||||||
<span className="flex w-40 items-center truncate md:w-auto">
|
<Tooltip content={intl.formatMessage(messages.requestedby)}>
|
||||||
<Tooltip content={intl.formatMessage(messages.requestedby)}>
|
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
|
||||||
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
|
</Tooltip>
|
||||||
</Tooltip>
|
<span className="w-40 truncate md:w-auto">
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
request.requestedBy.id === user?.id
|
request.requestedBy.id === user?.id
|
||||||
? '/profile'
|
? '/profile'
|
||||||
: `/users/${request.requestedBy.id}`
|
: `/users/${request.requestedBy.id}`
|
||||||
}
|
}
|
||||||
className="flex items-center font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
||||||
>
|
>
|
||||||
<span className="avatar-sm">
|
|
||||||
<CachedImage
|
|
||||||
type="avatar"
|
|
||||||
src={request.requestedBy.avatar}
|
|
||||||
alt=""
|
|
||||||
className="avatar-sm object-cover"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
{request.requestedBy.displayName}
|
{request.requestedBy.displayName}
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{request.modifiedBy && (
|
{request.modifiedBy && (
|
||||||
<div className="flex flex-nowrap">
|
<div className="flex flex-nowrap">
|
||||||
<span className="flex w-40 items-center truncate md:w-auto">
|
<Tooltip content={intl.formatMessage(messages.lastmodifiedby)}>
|
||||||
<Tooltip
|
<EyeIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
|
||||||
content={intl.formatMessage(messages.lastmodifiedby)}
|
</Tooltip>
|
||||||
>
|
<span className="w-40 truncate md:w-auto">
|
||||||
<EyeIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
|
|
||||||
</Tooltip>
|
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
request.modifiedBy.id === user?.id
|
request.modifiedBy.id === user?.id
|
||||||
? '/profile'
|
? '/profile'
|
||||||
: `/users/${request.modifiedBy.id}`
|
: `/users/${request.modifiedBy.id}`
|
||||||
}
|
}
|
||||||
className="flex items-center font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
||||||
>
|
>
|
||||||
<span className="avatar-sm">
|
|
||||||
<CachedImage
|
|
||||||
type="avatar"
|
|
||||||
src={request.modifiedBy.avatar}
|
|
||||||
alt=""
|
|
||||||
className="avatar-sm object-cover"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
{request.modifiedBy.displayName}
|
{request.modifiedBy.displayName}
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -42,9 +42,6 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
|
|||||||
networkDisclaimer:
|
networkDisclaimer:
|
||||||
'Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.',
|
'Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.',
|
||||||
docs: 'documentation',
|
docs: 'documentation',
|
||||||
forceIpv4First: 'Force IPv4 Resolution First',
|
|
||||||
forceIpv4FirstTip:
|
|
||||||
'Force Jellyseerr to resolve IPv4 addresses first instead of IPv6',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const SettingsNetwork = () => {
|
const SettingsNetwork = () => {
|
||||||
@@ -89,7 +86,6 @@ const SettingsNetwork = () => {
|
|||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
csrfProtection: data?.csrfProtection,
|
csrfProtection: data?.csrfProtection,
|
||||||
forceIpv4First: data?.forceIpv4First,
|
|
||||||
trustProxy: data?.trustProxy,
|
trustProxy: data?.trustProxy,
|
||||||
proxyEnabled: data?.proxy?.enabled,
|
proxyEnabled: data?.proxy?.enabled,
|
||||||
proxyHostname: data?.proxy?.hostname,
|
proxyHostname: data?.proxy?.hostname,
|
||||||
@@ -106,7 +102,6 @@ const SettingsNetwork = () => {
|
|||||||
try {
|
try {
|
||||||
await axios.post('/api/v1/settings/network', {
|
await axios.post('/api/v1/settings/network', {
|
||||||
csrfProtection: values.csrfProtection,
|
csrfProtection: values.csrfProtection,
|
||||||
forceIpv4First: values.forceIpv4First,
|
|
||||||
trustProxy: values.trustProxy,
|
trustProxy: values.trustProxy,
|
||||||
proxy: {
|
proxy: {
|
||||||
enabled: values.proxyEnabled,
|
enabled: values.proxyEnabled,
|
||||||
@@ -198,29 +193,6 @@ const SettingsNetwork = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="form-row">
|
||||||
<label htmlFor="proxyEnabled" className="checkbox-label">
|
<label htmlFor="proxyEnabled" className="checkbox-label">
|
||||||
<span className="mr-2">
|
<span className="mr-2">
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ import defineMessages from '@app/utils/defineMessages';
|
|||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
import {
|
import {
|
||||||
BarsArrowDownIcon,
|
BarsArrowDownIcon,
|
||||||
|
BarsArrowUpIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
InboxArrowDownIcon,
|
InboxArrowDownIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
@@ -77,14 +80,27 @@ const messages = defineMessages('components.UserList', {
|
|||||||
autogeneratepasswordTip: 'Email a server-generated password to the user',
|
autogeneratepasswordTip: 'Email a server-generated password to the user',
|
||||||
validationUsername: 'You must provide an username',
|
validationUsername: 'You must provide an username',
|
||||||
validationEmail: 'Email required',
|
validationEmail: 'Email required',
|
||||||
sortCreated: 'Join Date',
|
sortBy: 'Sort by {field}',
|
||||||
sortDisplayName: 'Display Name',
|
sortByUser: 'Sort by username',
|
||||||
sortRequests: 'Request Count',
|
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:
|
localLoginDisabled:
|
||||||
'The <strong>Enable Local Sign-In</strong> setting is currently disabled.',
|
'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 UserList = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -94,10 +110,12 @@ const UserList = () => {
|
|||||||
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
|
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
|
||||||
const [currentSort, setCurrentSort] = useState<Sort>('displayname');
|
const [currentSort, setCurrentSort] = useState<Sort>('displayname');
|
||||||
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
||||||
|
const [localUsers, setLocalUsers] = useState<User[]>([]);
|
||||||
|
|
||||||
const page = router.query.page ? Number(router.query.page) : 1;
|
const page = router.query.page ? Number(router.query.page) : 1;
|
||||||
const pageIndex = page - 1;
|
const pageIndex = page - 1;
|
||||||
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
||||||
|
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -106,9 +124,59 @@ const UserList = () => {
|
|||||||
} = useSWR<UserResultsResponse>(
|
} = useSWR<UserResultsResponse>(
|
||||||
`/api/v1/user?take=${currentPageSize}&skip=${
|
`/api/v1/user?take=${currentPageSize}&skip=${
|
||||||
pageIndex * currentPageSize
|
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 [isDeleting, setDeleting] = useState(false);
|
||||||
const [showImportModal, setShowImportModal] = useState(false);
|
const [showImportModal, setShowImportModal] = useState(false);
|
||||||
const [deleteModal, setDeleteModal] = useState<{
|
const [deleteModal, setDeleteModal] = useState<{
|
||||||
@@ -133,6 +201,9 @@ const UserList = () => {
|
|||||||
|
|
||||||
setCurrentSort(filterSettings.currentSort);
|
setCurrentSort(filterSettings.currentSort);
|
||||||
setCurrentPageSize(filterSettings.currentPageSize);
|
setCurrentPageSize(filterSettings.currentPageSize);
|
||||||
|
if (filterSettings.sortDirection) {
|
||||||
|
setSortDirection(filterSettings.sortDirection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -142,9 +213,74 @@ const UserList = () => {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
currentSort,
|
currentSort,
|
||||||
currentPageSize,
|
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) =>
|
const isUserPermsEditable = (userId: number) =>
|
||||||
userId !== 1 && userId !== currentUser?.id;
|
userId !== 1 && userId !== currentUser?.id;
|
||||||
@@ -541,28 +677,47 @@ const UserList = () => {
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-2 flex flex-grow lg:mb-0 lg:flex-grow-0">
|
<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">
|
<button
|
||||||
<BarsArrowDownIcon className="h-6 w-6" />
|
type="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"
|
||||||
|
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
|
<select
|
||||||
id="sort"
|
id="sort"
|
||||||
name="sort"
|
name="sort"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setCurrentSort(e.target.value as Sort);
|
setCurrentSort(e.target.value as Sort);
|
||||||
router.push(router.pathname);
|
|
||||||
}}
|
}}
|
||||||
value={currentSort}
|
value={currentSort}
|
||||||
className="rounded-r-only"
|
className="rounded-r-only"
|
||||||
>
|
>
|
||||||
<option value="created">
|
<option value="displayname">
|
||||||
{intl.formatMessage(messages.sortCreated)}
|
{intl.formatMessage(messages.username)}
|
||||||
</option>
|
</option>
|
||||||
<option value="requests">
|
<option value="requests">
|
||||||
{intl.formatMessage(messages.sortRequests)}
|
{intl.formatMessage(messages.totalrequests)}
|
||||||
</option>
|
</option>
|
||||||
<option value="displayname">
|
<option value="usertype">
|
||||||
{intl.formatMessage(messages.sortDisplayName)}
|
{intl.formatMessage(messages.accounttype)}
|
||||||
|
</option>
|
||||||
|
<option value="role">{intl.formatMessage(messages.role)}</option>
|
||||||
|
<option value="created">
|
||||||
|
{intl.formatMessage(messages.created)}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -584,11 +739,46 @@ const UserList = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Table.TH>
|
</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.user)}</Table.TH>
|
<SortableColumnHeader
|
||||||
<Table.TH>{intl.formatMessage(messages.totalrequests)}</Table.TH>
|
sortKey="displayname"
|
||||||
<Table.TH>{intl.formatMessage(messages.accounttype)}</Table.TH>
|
currentSort={currentSort}
|
||||||
<Table.TH>{intl.formatMessage(messages.role)}</Table.TH>
|
sortDirection={sortDirection}
|
||||||
<Table.TH>{intl.formatMessage(messages.created)}</Table.TH>
|
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">
|
<Table.TH className="text-right">
|
||||||
{(data.results ?? []).length > 1 && (
|
{(data.results ?? []).length > 1 && (
|
||||||
<Button
|
<Button
|
||||||
@@ -604,7 +794,7 @@ const UserList = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<Table.TBody>
|
<Table.TBody>
|
||||||
{data?.results.map((user) => (
|
{localUsers.map((user) => (
|
||||||
<tr key={`user-list-${user.id}`} data-testid="user-list-row">
|
<tr key={`user-list-${user.id}`} data-testid="user-list-row">
|
||||||
<Table.TD>
|
<Table.TD>
|
||||||
{isUserPermsEditable(user.id) && (
|
{isUserPermsEditable(user.id) && (
|
||||||
|
|||||||
@@ -160,12 +160,9 @@ const UserProfile = () => {
|
|||||||
<dd className="mt-1 text-3xl font-semibold text-white">
|
<dd className="mt-1 text-3xl font-semibold text-white">
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
currentHasPermission(
|
user.id === currentUser?.id
|
||||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
? '/profile/requests?filter=all'
|
||||||
{ type: 'or' }
|
: `/users/${user?.id}/requests?filter=all`
|
||||||
)
|
|
||||||
? `/users/${user?.id}/requests?filter=all`
|
|
||||||
: '/requests'
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{intl.formatNumber(user.requestCount)}
|
{intl.formatNumber(user.requestCount)}
|
||||||
@@ -296,12 +293,9 @@ const UserProfile = () => {
|
|||||||
<div className="slider-header">
|
<div className="slider-header">
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
currentHasPermission(
|
user.id === currentUser?.id
|
||||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
? '/profile/requests?filter=all'
|
||||||
{ type: 'or' }
|
: `/users/${user?.id}/requests?filter=all`
|
||||||
)
|
|
||||||
? `/users/${user?.id}/requests?filter=all`
|
|
||||||
: '/requests'
|
|
||||||
}
|
}
|
||||||
className="slider-title"
|
className="slider-title"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -978,13 +978,10 @@
|
|||||||
"components.Settings.SettingsMain.validationUrl": "You must provide a valid URL",
|
"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.validationUrlTrailingSlash": "URL must not end in a trailing slash",
|
||||||
"components.Settings.SettingsMain.youtubeUrl": "YouTube URL",
|
"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.csrfProtection": "Enable CSRF Protection",
|
||||||
"components.Settings.SettingsNetwork.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
|
"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.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
|
||||||
"components.Settings.SettingsNetwork.docs": "documentation",
|
"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.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.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",
|
"components.Settings.SettingsNetwork.networksettings": "Network Settings",
|
||||||
@@ -1216,7 +1213,7 @@
|
|||||||
"components.Setup.librarieserror": "Validation failed. Please toggle the libraries again to continue.",
|
"components.Setup.librarieserror": "Validation failed. Please toggle the libraries again to continue.",
|
||||||
"components.Setup.servertype": "Choose Server Type",
|
"components.Setup.servertype": "Choose Server Type",
|
||||||
"components.Setup.setup": "Setup",
|
"components.Setup.setup": "Setup",
|
||||||
"components.Setup.signin": "Sign In",
|
"components.Setup.signin": "Sign in to your account",
|
||||||
"components.Setup.signinMessage": "Get started by signing in",
|
"components.Setup.signinMessage": "Get started by signing in",
|
||||||
"components.Setup.signinWithEmby": "Enter your Emby details",
|
"components.Setup.signinWithEmby": "Enter your Emby details",
|
||||||
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
|
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
|
||||||
@@ -1285,6 +1282,7 @@
|
|||||||
"components.TvDetails.watchtrailer": "Watch Trailer",
|
"components.TvDetails.watchtrailer": "Watch Trailer",
|
||||||
"components.UserList.accounttype": "Type",
|
"components.UserList.accounttype": "Type",
|
||||||
"components.UserList.admin": "Admin",
|
"components.UserList.admin": "Admin",
|
||||||
|
"components.UserList.ascending": "ascending",
|
||||||
"components.UserList.autogeneratepassword": "Automatically Generate Password",
|
"components.UserList.autogeneratepassword": "Automatically Generate Password",
|
||||||
"components.UserList.autogeneratepasswordTip": "Email a server-generated password to the user",
|
"components.UserList.autogeneratepasswordTip": "Email a server-generated password to the user",
|
||||||
"components.UserList.bulkedit": "Bulk Edit",
|
"components.UserList.bulkedit": "Bulk Edit",
|
||||||
@@ -1294,6 +1292,7 @@
|
|||||||
"components.UserList.creating": "Creating…",
|
"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.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.deleteuser": "Delete User",
|
||||||
|
"components.UserList.descending": "descending",
|
||||||
"components.UserList.edituser": "Edit User Permissions",
|
"components.UserList.edituser": "Edit User Permissions",
|
||||||
"components.UserList.email": "Email Address",
|
"components.UserList.email": "Email Address",
|
||||||
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!",
|
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!",
|
||||||
@@ -1315,9 +1314,13 @@
|
|||||||
"components.UserList.passwordinfodescription": "Configure an application URL and enable email notifications to allow automatic password generation.",
|
"components.UserList.passwordinfodescription": "Configure an application URL and enable email notifications to allow automatic password generation.",
|
||||||
"components.UserList.plexuser": "Plex User",
|
"components.UserList.plexuser": "Plex User",
|
||||||
"components.UserList.role": "Role",
|
"components.UserList.role": "Role",
|
||||||
"components.UserList.sortCreated": "Join Date",
|
"components.UserList.sortBy": "Sort by {field}",
|
||||||
"components.UserList.sortDisplayName": "Display Name",
|
"components.UserList.sortByJoined": "Sort by join date",
|
||||||
"components.UserList.sortRequests": "Request Count",
|
"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.totalrequests": "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.",
|
||||||
|
|||||||
8
src/pages/profile/requests.tsx
Normal file
8
src/pages/profile/requests.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import RequestList from '@app/components/RequestList';
|
||||||
|
import type { NextPage } from 'next';
|
||||||
|
|
||||||
|
const UserRequestsPage: NextPage = () => {
|
||||||
|
return <RequestList />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserRequestsPage;
|
||||||
Reference in New Issue
Block a user