Skip to content

Commit f5ada6a

Browse files
authored
Feature/add download (#1004)
* fix media file cell * fix build * Update MediaFilesCell.tsx * add pnpm * Rework download and columns
1 parent 9452e7d commit f5ada6a

File tree

3 files changed

+113
-94
lines changed

3 files changed

+113
-94
lines changed

web/src/features/responses/components/MediaFilesCell/MediaFilesCell.tsx

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button';
33
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
44
import { cn, getFileCategory } from '@/lib/utils';
55
import { ArrowDownTrayIcon, DocumentIcon, FilmIcon, MusicalNoteIcon, PhotoIcon } from '@heroicons/react/24/outline';
6+
import axios from 'axios';
67
import { useMemo } from 'react';
78
import ReactPlayer from 'react-player';
89
import { Attachment } from '../../models/common';
@@ -17,30 +18,27 @@ export default function MediaFilesCell({ attachment, className }: MediaFilesCell
1718
return getFileCategory(attachment.mimeType);
1819
}, [attachment.mimeType]);
1920

20-
// const handleDownload = async (e: React.MouseEvent) => {
21-
// e.stopPropagation();
22-
// try {
23-
// // throw new Error("uncomment this line to mock failure of API");
24-
// const response = await axios.get(attachment.presignedUrl, {
25-
// responseType: 'blob',
26-
// headers: {
27-
// 'Access-Control-Allow-Origin': '*',
28-
// },
29-
// });
21+
const handleDownload = async (e: React.MouseEvent) => {
22+
e.stopPropagation();
23+
try {
24+
const response = await axios.get(attachment.presignedUrl, {
25+
responseType: 'blob',
26+
headers: {},
27+
});
3028

31-
// // Create download link
32-
// const url = window.URL.createObjectURL(new Blob([response.data]));
33-
// const link = document.createElement('a');
34-
// link.href = url;
35-
// link.download = attachment.fileName;
36-
// document.body.appendChild(link);
37-
// link.click();
38-
// document.body.removeChild(link);
39-
// window.URL.revokeObjectURL(url);
40-
// } catch (error) {
41-
// console.error('Download failed:', error);
42-
// }
43-
// };
29+
// Create download link
30+
const url = window.URL.createObjectURL(new Blob([response.data]));
31+
const link = document.createElement('a');
32+
link.href = url;
33+
link.download = attachment.fileName;
34+
document.body.appendChild(link);
35+
link.click();
36+
document.body.removeChild(link);
37+
window.URL.revokeObjectURL(url);
38+
} catch (error) {
39+
console.error('Download failed:', error);
40+
}
41+
};
4442

4543
const renderPreview = (isInDialog = false) => {
4644
const baseClasses = 'object-cover rounded-md transition-opacity duration-200';
@@ -116,7 +114,7 @@ export default function MediaFilesCell({ attachment, className }: MediaFilesCell
116114

117115
return (
118116
<Dialog>
119-
<DialogTrigger asChild>
117+
<DialogTrigger asChild className='w-full'>
120118
<div className={cn('group relative', className)}>
121119
<AspectRatio ratio={16 / 9}>{renderPreview(false)}</AspectRatio>
122120

@@ -147,14 +145,12 @@ export default function MediaFilesCell({ attachment, className }: MediaFilesCell
147145
</div>
148146

149147
<Button
150-
// onClick={handleDownload}
148+
onClick={handleDownload}
151149
variant='outline'
152150
size='sm'
153151
className='flex items-center gap-2 shrink-0 ml-4'>
154152
<ArrowDownTrayIcon className='w-4 h-4' />
155-
<a href={attachment.presignedUrl} target='_blank' rel='noopener noreferrer'>
156-
Download
157-
</a>
153+
Download
158154
</Button>
159155
</div>
160156
</div>

web/src/features/responses/components/ResponseExtraDataTable/ResponseExtraDataTable.tsx

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,60 @@ type ResponseExtraDataTableProps = {
88
data: QuestionExtraData[];
99
};
1010

11+
import { getFilteredRowModel, SortingState } from '@tanstack/react-table';
12+
import * as React from 'react';
13+
1114
export function ResponseExtraDataTable({ columns, data }: ResponseExtraDataTableProps): FunctionComponent {
15+
const [sorting, setSorting] = React.useState<SortingState>([]);
16+
1217
const table = useReactTable({
13-
columns,
1418
data,
19+
columns,
20+
onSortingChange: setSorting,
1521
getCoreRowModel: getCoreRowModel(),
1622
getSortedRowModel: getSortedRowModel(),
23+
getFilteredRowModel: getFilteredRowModel(),
24+
state: {
25+
sorting,
26+
},
1727
});
1828

19-
const rows = table.getRowModel().rows;
20-
2129
return (
22-
<>
23-
<Table>
24-
<TableHeader>
25-
{table.getHeaderGroups().map((headerGroup) => (
26-
<TableRow key={headerGroup.id}>
27-
{headerGroup.headers.map((header) => {
28-
return (
29-
<TableHead key={header.id} style={{ width: `${header.getSize()}px` }}>
30-
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
31-
</TableHead>
32-
);
33-
})}
34-
</TableRow>
35-
))}
36-
</TableHeader>
37-
38-
<TableBody>
39-
{rows.length > 0 ? (
40-
table.getRowModel().rows.map((row) => (
41-
<TableRow key={row.id}>
42-
{row.getVisibleCells().map((cell) => (
43-
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
44-
))}
30+
<div className='w-full'>
31+
<div className='overflow-hidden rounded-md border'>
32+
<Table>
33+
<TableHeader>
34+
{table.getHeaderGroups().map((headerGroup) => (
35+
<TableRow key={headerGroup.id}>
36+
{headerGroup.headers.map((header) => {
37+
return (
38+
<TableHead key={header.id} style={{ width: `${header.getSize()}px` }}>
39+
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
40+
</TableHead>
41+
);
42+
})}
43+
</TableRow>
44+
))}
45+
</TableHeader>
46+
<TableBody>
47+
{table.getRowModel().rows?.length ? (
48+
table.getRowModel().rows.map((row) => (
49+
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
50+
{row.getVisibleCells().map((cell) => (
51+
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
52+
))}
53+
</TableRow>
54+
))
55+
) : (
56+
<TableRow>
57+
<TableCell colSpan={columns.length} className='h-24 text-center'>
58+
No results.
59+
</TableCell>
4560
</TableRow>
46-
))
47-
) : (
48-
<TableRow>
49-
<TableCell className='h-24 text-center' colSpan={columns.length}>
50-
No results.
51-
</TableCell>
52-
</TableRow>
53-
)}
54-
</TableBody>
55-
</Table>
56-
</>
61+
)}
62+
</TableBody>
63+
</Table>
64+
</div>
65+
</div>
5766
);
5867
}

web/src/features/responses/utils/column-defs.tsx

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
import TableTagList from '@/components/table-tag-list/TableTagList';
2-
import { Badge } from '@/components/ui/badge';
3-
import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumnHeader';
4-
import { cn } from '@/lib/utils';
5-
import { ArrowTopRightOnSquareIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
6-
import { Link } from '@tanstack/react-router';
7-
import { format } from 'date-fns';
8-
91
import { DateTimeFormat } from '@/common/formats';
102
import {
113
CitizenReportFollowUpStatus,
124
FormSubmissionFollowUpStatus,
135
IncidentReportFollowUpStatus,
146
QuickReportFollowUpStatus,
157
} from '@/common/types';
8+
import TableTagList from '@/components/table-tag-list/TableTagList';
9+
import { Badge } from '@/components/ui/badge';
1610
import { Button } from '@/components/ui/button';
1711
import type { RowData } from '@/components/ui/DataTable/DataTable';
12+
import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumnHeader';
13+
import { cn } from '@/lib/utils';
14+
import { ArrowTopRightOnSquareIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
15+
import { Link } from '@tanstack/react-router';
1816
import type { ColumnDef } from '@tanstack/react-table';
17+
import { format } from 'date-fns';
18+
import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react';
19+
import MediaFilesCell from '../components/MediaFilesCell/MediaFilesCell';
1920
import { CitizenReportsAggregatedByForm, type CitizenReportByEntry } from '../models/citizen-report';
2021
import { SubmissionType } from '../models/common';
2122
import {
@@ -27,7 +28,6 @@ import { IncidentReportByEntry, IncidentReportByForm, IncidentReportByObserver }
2728
import { type QuickReport } from '../models/quick-report';
2829
import type { QuestionExtraData } from '../types';
2930
import { mapIncidentCategory, mapIncidentReportLocationType, mapQuickReportLocationType } from './helpers';
30-
import MediaFilesCell from '../components/MediaFilesCell/MediaFilesCell';
3131

3232
export const formSubmissionsByEntryColumnDefs: ColumnDef<FormSubmissionByEntry & RowData>[] = [
3333
{
@@ -518,35 +518,49 @@ export const formSubmissionsByFormColumnDefs: ColumnDef<FormSubmissionByForm & R
518518

519519
export const answerExtraInfoColumnDefs: ColumnDef<QuestionExtraData>[] = [
520520
{
521-
header: ({ column }) => <DataTableColumnHeader title='Type' column={column} className='w-[70px]' />,
522-
accessorFn: (row) => row.type,
523-
id: 'type',
524-
enableSorting: true,
525-
enableGlobalFilter: true,
526-
cell: ({ row }) => <div className='w-[80px]'>{row.original.type}</div>,
527-
size: 80, // fixed width in px
528-
minSize: 60, // minimum allowed width
529-
maxSize: 120, // optional max width
521+
accessorKey: 'type',
522+
header: () => <div className='w-[90px]'>Type</div>,
523+
cell: ({ row }) => <div className='w-[80px]'>{row.getValue('type')}</div>,
524+
size: 80,
525+
enableResizing: false,
530526
},
531527
{
532-
header: ({ column }) => <DataTableColumnHeader title='Time submitted' className='w-[70px]' column={column} />,
533-
accessorFn: (row) => row.timeSubmitted,
534-
id: 'timeSubmitted',
535-
enableSorting: true,
536-
enableGlobalFilter: true,
537-
cell: ({ row }) => <div className='w-[80px]'>{format(row.original.timeSubmitted, DateTimeFormat)}</div>,
538-
size: 80, // fixed width in px
539-
minSize: 60, // minimum allowed width
540-
maxSize: 120, // optional max width
528+
accessorKey: 'timeSubmitted',
529+
header: ({ column }) => {
530+
const isSorted = column.getIsSorted();
531+
532+
return (
533+
<Button variant='ghost' onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}>
534+
Time submitted
535+
{isSorted === 'asc' ? (
536+
<ArrowUp className='w-4 h-4' />
537+
) : isSorted === 'desc' ? (
538+
<ArrowDown className='w-4 h-4' />
539+
) : (
540+
<ArrowUpDown className='w-4 h-4' />
541+
)}
542+
</Button>
543+
);
544+
},
545+
cell: ({ row }) => {
546+
const formatted = format(row.original.timeSubmitted, DateTimeFormat);
547+
548+
return <div className=''>{formatted}</div>;
549+
},
550+
size: 80,
551+
enableResizing: false,
541552
},
553+
542554
{
543-
header: ({ column }) => <DataTableColumnHeader title='Preview' column={column} />,
544555
id: 'preview',
545556
enableSorting: false,
546557
enableGlobalFilter: false,
547558
cell: ({ row }) => (
548-
<div>{row.original.type === 'Note' ? row.original.text : <MediaFilesCell attachment={row.original} />}</div>
559+
<div className='w-full'>
560+
{row.original.type === 'Note' ? row.original.text : <MediaFilesCell attachment={row.original} />}
561+
</div>
549562
),
563+
size: 300,
550564
},
551565
];
552566

0 commit comments

Comments
 (0)