Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
455eef9f
Unverified
Commit
455eef9f
authored
Nov 24, 2025
by
Richard Szegh
Committed by
GitHub
Nov 24, 2025
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(web): add ability to delete unused attachments (#5272)
parent
60d977c0
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
59 additions
and
4 deletions
+59
-4
en.json
web/src/locales/en.json
+2
-0
Attachments.tsx
web/src/pages/Attachments.tsx
+57
-4
No files found.
web/src/locales/en.json
View file @
455eef9f
...
...
@@ -231,6 +231,8 @@
"delete-selected-resources"
:
"Delete Selected Resources"
,
"delete-all-unused"
:
"Delete all unused"
,
"delete-all-unused-confirm"
:
"Are you sure you want to delete all unused resources? THIS ACTION IS IRREVERSIBLE"
,
"delete-all-unused-success"
:
"Resources deleted successfully"
,
"delete-all-unused-error"
:
"Failed to delete unused resources"
,
"fetching-data"
:
"Fetching data…"
,
"file-drag-drop-prompt"
:
"Drag and drop your file here to upload file"
,
"linked-amount"
:
"Linked amount"
,
...
...
web/src/pages/Attachments.tsx
View file @
455eef9f
import
dayjs
from
"dayjs"
;
import
{
includes
}
from
"lodash-es"
;
import
{
PaperclipIcon
,
SearchIcon
}
from
"lucide-react"
;
import
{
PaperclipIcon
,
SearchIcon
,
Trash
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
AttachmentIcon
from
"@/components/AttachmentIcon"
;
import
ConfirmDialog
from
"@/components/ConfirmDialog"
;
import
Empty
from
"@/components/Empty"
;
import
MobileHeader
from
"@/components/MobileHeader"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
attachmentServiceClient
}
from
"@/grpcweb"
;
import
useDialog
from
"@/hooks/useDialog"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
i18n
from
"@/i18n"
;
import
{
attachmentStore
}
from
"@/store"
;
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
...
...
@@ -39,6 +42,7 @@ const Attachments = observer(() => {
const
t
=
useTranslate
();
const
{
md
}
=
useResponsiveWidth
();
const
loadingState
=
useLoading
();
const
deleteUnusedAttachmentsDialog
=
useDialog
();
const
[
state
,
setState
]
=
useState
<
State
>
({
searchQuery
:
""
,
});
...
...
@@ -88,6 +92,37 @@ const Attachments = observer(() => {
}
};
const
handleRefetch
=
async
()
=>
{
try
{
loadingState
.
setLoading
();
const
{
attachments
:
fetchedAttachments
,
nextPageToken
}
=
await
attachmentServiceClient
.
listAttachments
({
pageSize
:
50
,
});
setAttachments
(
fetchedAttachments
);
setNextPageToken
(
nextPageToken
??
""
);
loadingState
.
setFinish
();
}
catch
(
error
)
{
console
.
error
(
error
);
loadingState
.
setError
();
}
};
const
handleDeleteUnusedAttachments
=
async
()
=>
{
try
{
await
Promise
.
all
(
unusedAttachments
.
map
((
attachment
)
=>
{
return
attachmentStore
.
deleteAttachment
(
attachment
.
name
);
}),
);
toast
.
success
(
t
(
"resource.delete-all-unused-success"
));
}
catch
(
error
)
{
console
.
error
(
error
);
toast
.
error
(
t
(
"resource.delete-all-unused-error"
));
}
finally
{
void
handleRefetch
();
}
};
return
(
<
section
className=
"@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8"
>
{
!
md
&&
<
MobileHeader
/>
}
...
...
@@ -158,9 +193,17 @@ const Attachments = observer(() => {
<
div
className=
"w-full flex flex-row justify-start items-start"
>
<
div
className=
"w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start"
></
div
>
<
div
className=
"w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap"
>
<
div
className=
"w-full flex flex-row justify-start items-center gap-2"
>
<
span
className=
"text-muted-foreground"
>
{
t
(
"resource.unused-resources"
)
}
</
span
>
<
span
className=
"text-muted-foreground opacity-80"
>
(
{
unusedAttachments
.
length
}
)
</
span
>
<
div
className=
"w-full flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2"
>
<
div
className=
"flex flex-row items-center gap-2"
>
<
span
className=
"text-muted-foreground"
>
{
t
(
"resource.unused-resources"
)
}
</
span
>
<
span
className=
"text-muted-foreground opacity-80"
>
(
{
unusedAttachments
.
length
}
)
</
span
>
</
div
>
<
div
>
<
Button
variant=
"destructive"
onClick=
{
()
=>
deleteUnusedAttachmentsDialog
.
open
()
}
size=
"sm"
>
<
Trash
/>
{
t
(
"resource.delete-all-unused"
)
}
</
Button
>
</
div
>
</
div
>
{
unusedAttachments
.
map
((
attachment
)
=>
{
return
(
...
...
@@ -193,6 +236,16 @@ const Attachments = observer(() => {
</
div
>
</
div
>
</
div
>
<
ConfirmDialog
open=
{
deleteUnusedAttachmentsDialog
.
isOpen
}
onOpenChange=
{
deleteUnusedAttachmentsDialog
.
setOpen
}
title=
{
t
(
"resource.delete-all-unused-confirm"
)
}
confirmLabel=
{
t
(
"common.delete"
)
}
cancelLabel=
{
t
(
"common.cancel"
)
}
onConfirm=
{
handleDeleteUnusedAttachments
}
confirmVariant=
"destructive"
/>
</
section
>
);
});
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment