feat(issue): add issue description preview (#1881)

* feat(issue): add issue description preview

This PR adds a description preview to the issues list page, allowing users to quickly view issue details without navigating to individual issue pages.

Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>

* fix(issue): remove unnecessary user join

Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>

---------

Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
This commit is contained in:
0xsysr3ll
2025-09-04 10:24:08 +02:00
committed by GitHub
parent 22b2824441
commit 13fe4c890b
3 changed files with 43 additions and 2 deletions

View File

@@ -54,6 +54,7 @@ issueRoutes.get<Record<string, string>, IssueResultsResponse>(
.leftJoinAndSelect('issue.createdBy', 'createdBy')
.leftJoinAndSelect('issue.media', 'media')
.leftJoinAndSelect('issue.modifiedBy', 'modifiedBy')
.leftJoinAndSelect('issue.comments', 'comments')
.where('issue.status IN (:...issueStatus)', {
issueStatus: statusFilter,
});

View File

@@ -1,6 +1,7 @@
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import Tooltip from '@app/components/Common/Tooltip';
import { issueOptions } from '@app/components/IssueModal/constants';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
@@ -26,6 +27,7 @@ const messages = defineMessages('components.IssueList.IssueItem', {
opened: 'Opened',
viewissue: 'View Issue',
unknownissuetype: 'Unknown',
descriptionpreview: 'Issue Description',
});
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
@@ -107,8 +109,15 @@ const IssueItem = ({ issue }: IssueItemProps) => {
}
}
const description = issue.comments?.[0]?.message || '';
const maxDescriptionLength = 120;
const shouldTruncate = description.length > maxDescriptionLength;
const truncatedDescription = shouldTruncate
? description.substring(0, maxDescriptionLength) + '...'
: description;
return (
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:flex-row">
{title.backdropPath && (
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
<CachedImage
@@ -168,8 +177,38 @@ const IssueItem = ({ issue }: IssueItemProps) => {
>
{isMovie(title) ? title.title : title.name}
</Link>
{description && (
<div className="mt-1 max-w-full">
<div className="overflow-hidden text-sm text-gray-300">
{shouldTruncate ? (
<Tooltip
content={
<div className="max-w-sm p-3">
<div className="mb-1 text-sm font-medium text-gray-200">
Issue Description
</div>
<div className="whitespace-pre-wrap text-sm leading-relaxed text-gray-300">
{description}
</div>
</div>
}
tooltipConfig={{
placement: 'top',
offset: [0, 8],
}}
>
<span className="block cursor-help truncate transition-colors hover:text-gray-200">
{truncatedDescription}
</span>
</Tooltip>
) : (
<span className="block break-words">{description}</span>
)}
</div>
</div>
)}
{problemSeasonEpisodeLine.length > 0 && (
<div className="card-field">
<div className="card-field mt-1">
{problemSeasonEpisodeLine.map((t, k) => (
<span key={k}>{t}</span>
))}

View File

@@ -180,6 +180,7 @@
"components.IssueDetails.toaststatusupdated": "Issue status updated successfully!",
"components.IssueDetails.toaststatusupdatefailed": "Something went wrong while updating the issue status.",
"components.IssueDetails.unknownissuetype": "Unknown",
"components.IssueList.IssueItem.descriptionpreview": "Issue Description",
"components.IssueList.IssueItem.episodes": "{episodeCount, plural, one {Episode} other {Episodes}}",
"components.IssueList.IssueItem.issuestatus": "Status",
"components.IssueList.IssueItem.issuetype": "Type",