1717// SPDX-License-Identifier: Apache-2.0
1818
1919import _ from "lodash" ;
20- import { useMemo } from "react" ;
20+ import { useCallback , useMemo , useState } from "react" ;
2121import { FormattedMessage } from "react-intl" ;
2222import { graphql , useFragment } from "react-relay/hooks" ;
23+ import { useMutation } from "react-relay" ;
2324
2425import type {
2526 FilesTable_FileEdgeFragment$data ,
2627 FilesTable_FileEdgeFragment$key ,
2728} from "@/api/__generated__/FilesTable_FileEdgeFragment.graphql" ;
2829
30+ import type { FilesTable_deleteFile_Mutation } from "@/api/__generated__/FilesTable_deleteFile_Mutation.graphql" ;
31+
2932import Button from "@/components/Button" ;
33+ import DeleteModal from "@/components/DeleteModal" ;
3034import Icon from "@/components/Icon" ;
3135import InfiniteTable from "@/components/InfiniteTable" ;
3236import { createColumnHelper } from "@/components/Table" ;
@@ -49,12 +53,60 @@ const FILES_TABLE_FRAGMENT = graphql`
4953 }
5054` ;
5155
56+ const DELETE_FILE_MUTATION = graphql `
57+ mutation FilesTable_deleteFile_Mutation($fileId: ID!) {
58+ deleteFile(id: $fileId) {
59+ result {
60+ id
61+ }
62+ }
63+ }
64+ ` ;
65+
66+ type FileActionsProps = {
67+ file : TableRecord ;
68+ onDeleteClick : ( file : TableRecord ) => void ;
69+ } ;
70+
71+ const FileActions = ( { file, onDeleteClick } : FileActionsProps ) => {
72+ const handleDownload = ( ) => {
73+ const url = file . baseFile . url ;
74+ if ( ! url ) return ;
75+
76+ const a = document . createElement ( "a" ) ;
77+ a . href = url ;
78+ a . download = file . name || "file" ;
79+ a . target = "_blank" ;
80+ document . body . appendChild ( a ) ;
81+ a . click ( ) ;
82+ a . remove ( ) ;
83+ } ;
84+
85+ return (
86+ < div className = "d-inline-flex align-items-center gap-2" >
87+ < Button
88+ className = "btn p-0 border-0 bg-transparent"
89+ onClick = { handleDownload }
90+ >
91+ < Icon className = "text-primary" icon = "arrowDown" />
92+ </ Button >
93+
94+ < Button
95+ className = "btn p-0 border-0 bg-transparent"
96+ onClick = { ( ) => onDeleteClick ( file ) }
97+ >
98+ < Icon className = "text-danger" icon = "delete" />
99+ </ Button >
100+ </ div >
101+ ) ;
102+ } ;
103+
52104type TableRecord = NonNullable <
53105 NonNullable < FilesTable_FileEdgeFragment$data > [ "edges" ]
54106> [ number ] [ "node" ] ;
55107
56108const columnHelper = createColumnHelper < TableRecord > ( ) ;
57- const getColumnsDefinition = ( ) => [
109+ const getColumnsDefinition = ( onDeleteClick : ( file : TableRecord ) => void ) => [
58110 columnHelper . accessor ( "name" , {
59111 header : ( ) => (
60112 < FormattedMessage
@@ -75,37 +127,22 @@ const getColumnsDefinition = () => [
75127 ) ,
76128 cell : ( { getValue } ) => {
77129 const size = getValue ( ) ;
78- if ( size == null ) return null ;
79- return formatFileSize ( size ) ;
130+ return size != null ? formatFileSize ( size ) : null ;
80131 } ,
81132 } ) ,
82133 columnHelper . accessor ( ( row ) => row , {
83134 id : "action" ,
84135 header : ( ) => (
85136 < FormattedMessage
86- id = "components.FilesTable.action "
87- defaultMessage = "Action "
137+ id = "components.FilesTable.actionsTitle "
138+ defaultMessage = "Actions "
88139 />
89140 ) ,
90- cell : ( { row } ) => (
91- < Button
92- className = "btn p-0 border-0 bg-transparent ms-4"
93- onClick = { ( ) => {
94- const url = row . original . baseFile . url ;
95- if ( ! url ) return ;
96-
97- const a = document . createElement ( "a" ) ;
98- a . href = url ;
99- a . download = row . original . name || "file" ;
100- a . target = "_blank" ;
101- document . body . appendChild ( a ) ;
102- a . click ( ) ;
103- a . remove ( ) ;
104- } }
105- >
106- < Icon className = "text-primary" icon = { "arrowDown" } />
107- </ Button >
108- ) ,
141+ cell : ( { row } ) => {
142+ const file = row . original ;
143+
144+ return < FileActions file = { file } onDeleteClick = { onDeleteClick } /> ;
145+ } ,
109146 } ) ,
110147] ;
111148
@@ -127,17 +164,86 @@ const FilesTable = ({
127164 return _ . compact ( filesFragment ?. edges ?. map ( ( e ) => e ?. node ) ) ?? [ ] ;
128165 } , [ filesFragment ] ) ;
129166
130- const columns = useMemo ( ( ) => getColumnsDefinition ( ) , [ ] ) ;
167+ const [ fileToDelete , setFileToDelete ] = useState < TableRecord | null > ( null ) ;
168+ const [ errorFeedback , setErrorFeedback ] = useState < React . ReactNode > ( null ) ;
169+
170+ const columns = useMemo ( ( ) => getColumnsDefinition ( setFileToDelete ) , [ ] ) ;
171+
172+ const handleCancelDelete = useCallback ( ( ) => {
173+ setFileToDelete ( null ) ;
174+ setErrorFeedback ( null ) ;
175+ } , [ ] ) ;
176+
177+ const [ deleteFile , isDeletingFile ] =
178+ useMutation < FilesTable_deleteFile_Mutation > ( DELETE_FILE_MUTATION ) ;
179+
180+ const handleDeleteFile = useCallback ( ( ) => {
181+ if ( ! fileToDelete ) return ;
182+
183+ deleteFile ( {
184+ variables : { fileId : fileToDelete . id } ,
185+ onCompleted ( _data , errors ) {
186+ if ( errors ) {
187+ const errorMessages = errors
188+ . map ( ( error ) => error . message )
189+ . join ( ". \n" ) ;
190+ setErrorFeedback ( errorMessages ) ;
191+ return ;
192+ }
193+
194+ setErrorFeedback ( null ) ;
195+ setFileToDelete ( null ) ;
196+ } ,
197+ onError ( ) {
198+ setErrorFeedback (
199+ < FormattedMessage
200+ id = "components.FilesTable.deletionErrorFeedback"
201+ defaultMessage = "Could not delete the file, please try again."
202+ /> ,
203+ ) ;
204+ } ,
205+ updater ( store , response ) {
206+ const deletedId = response ?. deleteFile ?. result ?. id ;
207+ if ( ! deletedId ) return ;
208+
209+ store . delete ( deletedId ) ;
210+ } ,
211+ } ) ;
212+ } , [ deleteFile , fileToDelete ] ) ;
131213
132214 return (
133- < InfiniteTable
134- className = { className }
135- columns = { columns }
136- data = { files }
137- loading = { loading }
138- onLoadMore = { onLoadMore }
139- hideSearch
140- />
215+ < >
216+ < InfiniteTable
217+ className = { className }
218+ columns = { columns }
219+ data = { files }
220+ loading = { loading }
221+ onLoadMore = { onLoadMore }
222+ hideSearch
223+ />
224+ { fileToDelete && (
225+ < DeleteModal
226+ confirmText = { fileToDelete . name || "" }
227+ onCancel = { handleCancelDelete }
228+ onConfirm = { handleDeleteFile }
229+ isDeleting = { isDeletingFile }
230+ title = {
231+ < FormattedMessage
232+ id = "components.FilesTable.deleteModal.title"
233+ defaultMessage = "Delete File"
234+ />
235+ }
236+ >
237+ < p >
238+ < FormattedMessage
239+ id = "components.FilesTable.deleteModal.description"
240+ defaultMessage = "This action cannot be undone. This will permanently delete the file."
241+ />
242+ </ p >
243+ { errorFeedback && < p className = "text-danger" > { errorFeedback } </ p > }
244+ </ DeleteModal >
245+ ) }
246+ </ >
141247 ) ;
142248} ;
143249
0 commit comments