fix(frontend): more issues-related fixes (#2234)

* fix(frontend): more issues-related fixes

* fix: permission VIEW_ISSUES is also sufficient for viewing issues in slideover

* fix(frontend): only display issue notif types user is eligible to receive

* fix: don't display issues block in slideover if no open issues

* fix: move year out of link in issue details header

* fix: use 'view' global string for issue block button

* fix: issue/request/user list sort options
pull/2255/head
TheCatLady 3 years ago committed by GitHub
parent 8c49309c35
commit 3ec4a9c76e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,7 +13,7 @@ const Badge: React.FC<BadgeProps> = ({
children,
}) => {
const badgeStyle = [
'px-2 inline-flex text-xs leading-5 font-semibold rounded-full',
'px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap',
];
if (url) {

@ -8,6 +8,7 @@ import Link from 'next/link';
import React from 'react';
import { useIntl } from 'react-intl';
import type Issue from '../../../server/entity/Issue';
import globalMessages from '../../i18n/globalMessages';
import Button from '../Common/Button';
import { issueOptions } from '../IssueModal/constants';
@ -56,7 +57,7 @@ const IssueBlock: React.FC<IssueBlockProps> = ({ issue }) => {
<Link href={`/issues/${issue.id}`} passHref>
<Button buttonType="primary" buttonSize="sm" as="a">
<EyeIcon />
<span>View</span>
<span>{intl.formatMessage(globalMessages.view)}</span>
</Button>
</Link>
</div>

@ -44,7 +44,7 @@ const messages = defineMessages({
reopenissueandcomment: 'Reopen with Comment',
issuepagetitle: 'Issue',
playonplex: 'Play on Plex',
play4konplex: 'Play 4K on Plex',
play4konplex: 'Play in 4K on Plex',
openinarr: 'Open in {arr}',
openin4karr: 'Open in 4K {arr}',
toasteditdescriptionsuccess: 'Issue description edited successfully!',
@ -228,7 +228,7 @@ const IssueDetails: React.FC = () => {
<div className="media-title">
<div className="media-status">
{issueData.status === IssueStatus.OPEN && (
<Badge badgeType="primary">
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.open)}
</Badge>
)}
@ -244,15 +244,11 @@ const IssueDetails: React.FC = () => {
issueData.media.mediaType === MediaType.MOVIE ? 'movie' : 'tv'
}/${data.id}`}
>
<a className="hover:underline">
{title}{' '}
{releaseYear && (
<span className="media-year">
({releaseYear.slice(0, 4)})
</span>
)}
</a>
</Link>
<a className="hover:underline">{title}</a>
</Link>{' '}
{releaseYear && (
<span className="media-year">({releaseYear.slice(0, 4)})</span>
)}
</h1>
<span className="media-attributes">
{intl.formatMessage(messages.openedby, {

@ -183,7 +183,7 @@ const IssueItem: React.FC<IssueItemProps> = ({ issue }) => {
{intl.formatMessage(messages.issuestatus)}
</span>
{issue.status === IssueStatus.OPEN ? (
<Badge badgeType="primary">
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.open)}
</Badge>
) : (

@ -19,7 +19,7 @@ import IssueItem from './IssueItem';
const messages = defineMessages({
issues: 'Issues',
sortAdded: 'Request Date',
sortAdded: 'Most Recent',
sortModified: 'Last Modified',
showallissues: 'Show All Issues',
});

@ -9,9 +9,12 @@ import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { MediaStatus } from '../../../../server/constants/media';
import type Issue from '../../../../server/entity/Issue';
import { MovieDetails } from '../../../../server/models/Movie';
import { TvDetails } from '../../../../server/models/Tv';
import useSettings from '../../../hooks/useSettings';
import { Permission, useUser } from '../../../hooks/useUser';
import globalMessages from '../../../i18n/globalMessages';
import Button from '../../Common/Button';
import Modal from '../../Common/Modal';
@ -21,7 +24,9 @@ const messages = defineMessages({
validationMessageRequired: 'You must provide a description',
issomethingwrong: 'Is there a problem with {title}?',
whatswrong: "What's wrong?",
providedetail: 'Provide a detailed explanation of the issue.',
providedetail:
'Please provide a detailed explanation of the issue you encountered.',
extras: 'Extras',
season: 'Season {seasonNumber}',
episode: 'Episode {episodeNumber}',
allseasons: 'All Seasons',
@ -56,6 +61,8 @@ const CreateIssueModal: React.FC<CreateIssueModalProps> = ({
tmdbId,
}) => {
const intl = useIntl();
const settings = useSettings();
const { hasPermission } = useUser();
const { addToast } = useToasts();
const { data, error } = useSWR<MovieDetails | TvDetails>(
tmdbId ? `/api/v1/${mediaType}/${tmdbId}` : null
@ -65,6 +72,20 @@ const CreateIssueModal: React.FC<CreateIssueModalProps> = ({
return null;
}
const availableSeasons = (data?.mediaInfo?.seasons ?? [])
.filter(
(season) =>
season.status === MediaStatus.AVAILABLE ||
season.status === MediaStatus.PARTIALLY_AVAILABLE ||
(settings.currentSettings.series4kEnabled &&
hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], {
type: 'or',
}) &&
(season.status4k === MediaStatus.AVAILABLE ||
season.status4k === MediaStatus.PARTIALLY_AVAILABLE))
)
.map((season) => season.seasonNumber);
const CreateIssueModalSchema = Yup.object().shape({
message: Yup.string().required(
intl.formatMessage(messages.validationMessageRequired)
@ -76,7 +97,7 @@ const CreateIssueModal: React.FC<CreateIssueModalProps> = ({
initialValues={{
selectedIssue: issueOptions[0],
message: '',
problemSeason: 0,
problemSeason: availableSeasons.length === 1 ? availableSeasons[0] : 0,
problemEpisode: 0,
}}
validationSchema={CreateIssueModalSchema}
@ -162,18 +183,23 @@ const CreateIssueModal: React.FC<CreateIssueModalProps> = ({
as="select"
id="problemSeason"
name="problemSeason"
disabled={availableSeasons.length === 1}
>
<option value={0}>
{intl.formatMessage(messages.allseasons)}
</option>
{data.seasons.map((season) => (
{availableSeasons.length > 1 && (
<option value={0}>
{intl.formatMessage(messages.allseasons)}
</option>
)}
{availableSeasons.map((season) => (
<option
value={season.seasonNumber}
key={`problem-season-${season.seasonNumber}`}
value={season}
key={`problem-season-${season}`}
>
{intl.formatMessage(messages.season, {
seasonNumber: season.seasonNumber,
})}
{season === 0
? intl.formatMessage(messages.extras)
: intl.formatMessage(messages.season, {
seasonNumber: season,
})}
</option>
))}
</Field>

@ -19,6 +19,7 @@ import RequestBlock from '../RequestBlock';
const messages = defineMessages({
manageModalTitle: 'Manage {mediaType}',
manageModalIssues: 'Open Issues',
manageModalRequests: 'Requests',
manageModalNoRequests: 'No requests.',
manageModalClearMedia: 'Clear Media Data',
@ -77,6 +78,11 @@ const ManageSlideOver: React.FC<
revalidate();
};
const openIssues =
data.mediaInfo?.issues?.filter(
(issue) => issue.status === IssueStatus.OPEN
) ?? [];
return (
<SlideOver
show={show}
@ -155,14 +161,17 @@ const ManageSlideOver: React.FC<
)}
</div>
)}
{(data.mediaInfo?.issues ?? []).length > 0 && (
<>
<h3 className="mb-2 text-xl">Open Issues</h3>
<div className="mb-4 overflow-hidden bg-gray-600 rounded-md shadow">
<ul>
{data.mediaInfo?.issues
?.filter((issue) => issue.status === IssueStatus.OPEN)
.map((issue) => (
{hasPermission([Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES], {
type: 'or',
}) &&
openIssues.length > 0 && (
<>
<h3 className="mb-2 text-xl">
{intl.formatMessage(messages.manageModalIssues)}
</h3>
<div className="mb-4 overflow-hidden bg-gray-600 rounded-md shadow">
<ul>
{openIssues.map((issue) => (
<li
key={`manage-issue-${issue.id}`}
className="border-b border-gray-700 last:border-b-0"
@ -170,10 +179,10 @@ const ManageSlideOver: React.FC<
<IssueBlock issue={issue} />
</li>
))}
</ul>
</div>
</>
)}
</ul>
</div>
</>
)}
<h3 className="mb-2 text-xl">
{intl.formatMessage(messages.manageModalRequests)}
</h3>

@ -330,7 +330,14 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
onUpdate={() => revalidate()}
/>
{(data.mediaInfo?.status === MediaStatus.AVAILABLE ||
data.mediaInfo?.status4k === MediaStatus.AVAILABLE) &&
(settings.currentSettings.movie4kEnabled &&
hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{
type: 'or',
}
) &&
data.mediaInfo?.status4k === MediaStatus.AVAILABLE)) &&
hasPermission(
[Permission.CREATE_ISSUES, Permission.MANAGE_ISSUES],
{
@ -338,7 +345,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
}
) && (
<Button
buttonType="danger"
buttonType="warning"
className="ml-2 first:ml-0"
onClick={() => setShowIssueModal(true)}
>
@ -348,20 +355,26 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
{hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
buttonType="default"
className="ml-2 first:ml-0"
className="relative ml-2 first:ml-0"
onClick={() => setShowManager(true)}
>
<CogIcon className="!mr-0" />
{(
data.mediaInfo?.issues.filter(
(issue) => issue.status === IssueStatus.OPEN
) ?? []
).length > 0 && (
<>
<div className="absolute w-3 h-3 bg-red-600 rounded-full -right-1 -top-1" />
<div className="absolute w-3 h-3 bg-red-600 rounded-full -right-1 -top-1 animate-ping" />
</>
)}
{hasPermission(
[Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES],
{
type: 'or',
}
) &&
(
data.mediaInfo?.issues.filter(
(issue) => issue.status === IssueStatus.OPEN
) ?? []
).length > 0 && (
<>
<div className="absolute w-3 h-3 bg-red-600 rounded-full -right-1 -top-1" />
<div className="absolute w-3 h-3 bg-red-600 rounded-full -right-1 -top-1 animate-ping" />
</>
)}
</Button>
)}
</div>

@ -274,6 +274,11 @@ const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
: messages.issuecommentDescription
),
value: Notification.ISSUE_COMMENT,
hidden:
user &&
!hasPermission([Permission.MANAGE_ISSUES, Permission.CREATE_ISSUES], {
type: 'or',
}),
hasNotifyUser:
!user || hasPermission(Permission.MANAGE_ISSUES) ? false : true,
},
@ -286,6 +291,11 @@ const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
: messages.issueresolvedDescription
),
value: Notification.ISSUE_RESOLVED,
hidden:
user &&
!hasPermission([Permission.MANAGE_ISSUES, Permission.CREATE_ISSUES], {
type: 'or',
}),
hasNotifyUser: true,
},
];

@ -22,7 +22,7 @@ import RequestItem from './RequestItem';
const messages = defineMessages({
requests: 'Requests',
showallrequests: 'Show All Requests',
sortAdded: 'Request Date',
sortAdded: 'Most Recent',
sortModified: 'Last Modified',
});

@ -331,9 +331,14 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
is4kShowComplete={is4kComplete}
/>
{(data.mediaInfo?.status === MediaStatus.AVAILABLE ||
data.mediaInfo?.status4k === MediaStatus.AVAILABLE ||
data.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE ||
data?.mediaInfo?.status4k === MediaStatus.PARTIALLY_AVAILABLE) &&
(settings.currentSettings.series4kEnabled &&
hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], {
type: 'or',
}) &&
(data.mediaInfo?.status4k === MediaStatus.AVAILABLE ||
data?.mediaInfo?.status4k ===
MediaStatus.PARTIALLY_AVAILABLE))) &&
hasPermission(
[Permission.CREATE_ISSUES, Permission.MANAGE_ISSUES],
{
@ -341,7 +346,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
}
) && (
<Button
buttonType="danger"
buttonType="warning"
className="ml-2 first:ml-0"
onClick={() => setShowIssueModal(true)}
>
@ -351,20 +356,26 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
{hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
buttonType="default"
className="ml-2 first:ml-0"
className="relative ml-2 first:ml-0"
onClick={() => setShowManager(true)}
>
<CogIcon className="!mr-0" />
{(
data.mediaInfo?.issues.filter(
(issue) => issue.status === IssueStatus.OPEN
) ?? []
).length > 0 && (
<>
<div className="absolute w-3 h-3 bg-red-600 rounded-full -right-1 -top-1" />
<div className="absolute w-3 h-3 bg-red-600 rounded-full -right-1 -top-1 animate-ping" />
</>
)}
{hasPermission(
[Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES],
{
type: 'or',
}
) &&
(
data.mediaInfo?.issues.filter(
(issue) => issue.status === IssueStatus.OPEN
) ?? []
).length > 0 && (
<>
<div className="absolute w-3 h-3 bg-red-600 rounded-full -right-1 -top-1" />
<div className="absolute w-3 h-3 bg-red-600 rounded-full -right-1 -top-1 animate-ping" />
</>
)}
</Button>
)}
</div>

@ -46,8 +46,7 @@ const messages = defineMessages({
totalrequests: 'Requests',
accounttype: 'Type',
role: 'Role',
created: 'Created',
lastupdated: 'Updated',
created: 'Joined',
bulkedit: 'Bulk Edit',
owner: 'Owner',
admin: 'Admin',
@ -75,8 +74,7 @@ const messages = defineMessages({
autogeneratepassword: 'Automatically Generate Password',
autogeneratepasswordTip: 'Email a server-generated password to the user',
validationEmail: 'You must provide a valid email address',
sortCreated: 'Creation Date',
sortUpdated: 'Last Updated',
sortCreated: 'Join Date',
sortDisplayName: 'Display Name',
sortRequests: 'Request Count',
localLoginDisabled:
@ -91,7 +89,7 @@ const UserList: React.FC = () => {
const settings = useSettings();
const { addToast } = useToasts();
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
const [currentSort, setCurrentSort] = useState<Sort>('created');
const [currentSort, setCurrentSort] = useState<Sort>('displayname');
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
const page = router.query.page ? Number(router.query.page) : 1;
@ -522,9 +520,6 @@ const UserList: React.FC = () => {
<option value="created">
{intl.formatMessage(messages.sortCreated)}
</option>
<option value="updated">
{intl.formatMessage(messages.sortUpdated)}
</option>
<option value="requests">
{intl.formatMessage(messages.sortRequests)}
</option>
@ -556,7 +551,6 @@ const UserList: React.FC = () => {
<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>{intl.formatMessage(messages.lastupdated)}</Table.TH>
<Table.TH className="text-right">
{(data.results ?? []).length > 1 && (
<Button
@ -652,13 +646,6 @@ const UserList: React.FC = () => {
day: 'numeric',
})}
</Table.TD>
<Table.TD>
{intl.formatDate(user.updatedAt, {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</Table.TD>
<Table.TD alignText="right">
<Button
buttonType="warning"

@ -57,7 +57,7 @@
"components.IssueDetails.openedby": "#{issueId} opened {relativeTime} by {username}",
"components.IssueDetails.openin4karr": "Open in 4K {arr}",
"components.IssueDetails.openinarr": "Open in {arr}",
"components.IssueDetails.play4konplex": "Play 4K on Plex",
"components.IssueDetails.play4konplex": "Play in 4K on Plex",
"components.IssueDetails.playonplex": "Play on Plex",
"components.IssueDetails.problemepisode": "Affected Episode",
"components.IssueDetails.problemseason": "Affected Season",
@ -82,15 +82,16 @@
"components.IssueList.IssueItem.viewissue": "View Issue",
"components.IssueList.issues": "Issues",
"components.IssueList.showallissues": "Show All Issues",
"components.IssueList.sortAdded": "Request Date",
"components.IssueList.sortAdded": "Most Recent",
"components.IssueList.sortModified": "Last Modified",
"components.IssueModal.CreateIssueModal.allepisodes": "All Episodes",
"components.IssueModal.CreateIssueModal.allseasons": "All Seasons",
"components.IssueModal.CreateIssueModal.episode": "Episode {episodeNumber}",
"components.IssueModal.CreateIssueModal.extras": "Extras",
"components.IssueModal.CreateIssueModal.issomethingwrong": "Is there a problem with {title}?",
"components.IssueModal.CreateIssueModal.problemepisode": "Affected Episode",
"components.IssueModal.CreateIssueModal.problemseason": "Affected Season",
"components.IssueModal.CreateIssueModal.providedetail": "Provide a detailed explanation of the issue.",
"components.IssueModal.CreateIssueModal.providedetail": "Please provide a detailed explanation of the issue you encountered.",
"components.IssueModal.CreateIssueModal.reportissue": "Report an Issue",
"components.IssueModal.CreateIssueModal.season": "Season {seasonNumber}",
"components.IssueModal.CreateIssueModal.submitissue": "Submit Issue",
@ -134,6 +135,7 @@
"components.ManageSlideOver.downloadstatus": "Download Status",
"components.ManageSlideOver.manageModalClearMedia": "Clear Media Data",
"components.ManageSlideOver.manageModalClearMediaWarning": "* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.",
"components.ManageSlideOver.manageModalIssues": "Open Issues",
"components.ManageSlideOver.manageModalNoRequests": "No requests.",
"components.ManageSlideOver.manageModalRequests": "Requests",
"components.ManageSlideOver.manageModalTitle": "Manage {mediaType}",
@ -287,7 +289,7 @@
"components.RequestList.RequestItem.seasons": "{seasonCount, plural, one {Season} other {Seasons}}",
"components.RequestList.requests": "Requests",
"components.RequestList.showallrequests": "Show All Requests",
"components.RequestList.sortAdded": "Request Date",
"components.RequestList.sortAdded": "Most Recent",
"components.RequestList.sortModified": "Last Modified",
"components.RequestModal.AdvancedRequester.advancedoptions": "Advanced",
"components.RequestModal.AdvancedRequester.animenote": "* This series is an anime.",
@ -799,7 +801,7 @@
"components.UserList.autogeneratepasswordTip": "Email a server-generated password to the user",
"components.UserList.bulkedit": "Bulk Edit",
"components.UserList.create": "Create",
"components.UserList.created": "Created",
"components.UserList.created": "Joined",
"components.UserList.createlocaluser": "Create Local User",
"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.",
@ -810,7 +812,6 @@
"components.UserList.importedfromplex": "{userCount, plural, one {# new user} other {# new users}} imported from Plex successfully!",
"components.UserList.importfromplex": "Import Users from Plex",
"components.UserList.importfromplexerror": "Something went wrong while importing users from Plex.",
"components.UserList.lastupdated": "Updated",
"components.UserList.localLoginDisabled": "The <strong>Enable Local Sign-In</strong> setting is currently disabled.",
"components.UserList.localuser": "Local User",
"components.UserList.nouserstoimport": "No new users to import from Plex.",
@ -819,10 +820,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.sortCreated": "Creation Date",
"components.UserList.sortCreated": "Join Date",
"components.UserList.sortDisplayName": "Display Name",
"components.UserList.sortRequests": "Request Count",
"components.UserList.sortUpdated": "Last Updated",
"components.UserList.totalrequests": "Requests",
"components.UserList.user": "User",
"components.UserList.usercreatedfailed": "Something went wrong while creating the user.",

Loading…
Cancel
Save