forked from cardosofelipe/pragma-stack
- Replaced `next/navigation` with `@/lib/i18n/routing` across components, pages, and tests. - Removed redundant `locale` props from `ProjectWizard` and related pages. - Updated navigation to exclude explicit `locale` in paths. - Refactored tests to use mocks from `next-intl/navigation`.
182 lines
5.4 KiB
TypeScript
182 lines
5.4 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Project Issues List Page
|
|
*
|
|
* Displays filterable, sortable list of issues for a project.
|
|
* Supports bulk actions and sync with external trackers.
|
|
*
|
|
* @module app/[locale]/(authenticated)/projects/[id]/issues/page
|
|
*/
|
|
|
|
import { useState, use } from 'react';
|
|
import { useRouter } from '@/lib/i18n/routing';
|
|
import { Plus, Upload } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { IssueFilters, IssueTable, BulkActions, useIssues } from '@/features/issues';
|
|
import type { IssueFiltersType, IssueSort } from '@/features/issues';
|
|
|
|
interface ProjectIssuesPageProps {
|
|
params: Promise<{
|
|
locale: string;
|
|
id: string;
|
|
}>;
|
|
}
|
|
|
|
export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) {
|
|
const { id: projectId } = use(params);
|
|
const router = useRouter();
|
|
|
|
// Filter state
|
|
const [filters, setFilters] = useState<IssueFiltersType>({
|
|
status: 'all',
|
|
priority: 'all',
|
|
sprint: 'all',
|
|
assignee: 'all',
|
|
});
|
|
|
|
// Sort state
|
|
const [sort, setSort] = useState<IssueSort>({
|
|
field: 'updated_at',
|
|
direction: 'desc',
|
|
});
|
|
|
|
// Selection state
|
|
const [selectedIssues, setSelectedIssues] = useState<string[]>([]);
|
|
|
|
// Fetch issues
|
|
const { data, isLoading, error } = useIssues(projectId, filters, sort);
|
|
|
|
const handleIssueClick = (issueId: string) => {
|
|
router.push(`/projects/${projectId}/issues/${issueId}`);
|
|
};
|
|
|
|
const handleBulkChangeStatus = () => {
|
|
// TODO: Open status change dialog
|
|
console.log('Change status for:', selectedIssues);
|
|
};
|
|
|
|
const handleBulkAssign = () => {
|
|
// TODO: Open assign dialog
|
|
console.log('Assign:', selectedIssues);
|
|
};
|
|
|
|
const handleBulkAddLabels = () => {
|
|
// TODO: Open labels dialog
|
|
console.log('Add labels to:', selectedIssues);
|
|
};
|
|
|
|
const handleBulkDelete = () => {
|
|
// TODO: Confirm and delete
|
|
console.log('Delete:', selectedIssues);
|
|
};
|
|
|
|
const handleSync = () => {
|
|
// TODO: Sync all issues
|
|
console.log('Sync issues');
|
|
};
|
|
|
|
const handleNewIssue = () => {
|
|
// TODO: Navigate to new issue page or open dialog
|
|
console.log('Create new issue');
|
|
};
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="rounded-lg border border-destructive bg-destructive/10 p-6 text-center">
|
|
<h2 className="text-lg font-semibold text-destructive">Error Loading Issues</h2>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
Failed to load issues. Please try again later.
|
|
</p>
|
|
<Button variant="outline" className="mt-4" onClick={() => window.location.reload()}>
|
|
Retry
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-6">
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Issues</h1>
|
|
<p className="text-muted-foreground">
|
|
{isLoading ? (
|
|
<Skeleton className="h-4 w-24" />
|
|
) : (
|
|
`${data?.pagination.total || 0} issues found`
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" onClick={handleSync}>
|
|
<Upload className="mr-2 h-4 w-4" aria-hidden="true" />
|
|
Sync
|
|
</Button>
|
|
<Button size="sm" onClick={handleNewIssue}>
|
|
<Plus className="mr-2 h-4 w-4" aria-hidden="true" />
|
|
New Issue
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<IssueFilters filters={filters} onFiltersChange={setFilters} />
|
|
|
|
{/* Bulk Actions */}
|
|
<BulkActions
|
|
selectedCount={selectedIssues.length}
|
|
onChangeStatus={handleBulkChangeStatus}
|
|
onAssign={handleBulkAssign}
|
|
onAddLabels={handleBulkAddLabels}
|
|
onDelete={handleBulkDelete}
|
|
/>
|
|
|
|
{/* Issue Table */}
|
|
{isLoading ? (
|
|
<div className="space-y-2">
|
|
{[...Array(5)].map((_, i) => (
|
|
<Skeleton key={i} className="h-16 w-full" />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<IssueTable
|
|
issues={data?.data || []}
|
|
selectedIssues={selectedIssues}
|
|
onSelectionChange={setSelectedIssues}
|
|
onIssueClick={handleIssueClick}
|
|
sort={sort}
|
|
onSortChange={setSort}
|
|
/>
|
|
)}
|
|
|
|
{/* Pagination info */}
|
|
{data && data.pagination.total > 0 && (
|
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
<span>
|
|
Showing {(data.pagination.page - 1) * data.pagination.page_size + 1} to{' '}
|
|
{Math.min(data.pagination.page * data.pagination.page_size, data.pagination.total)} of{' '}
|
|
{data.pagination.total} issues
|
|
</span>
|
|
{data.pagination.total_pages > 1 && (
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" disabled={!data.pagination.has_prev}>
|
|
Previous
|
|
</Button>
|
|
<Button variant="outline" size="sm" disabled={!data.pagination.has_next}>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|