Skip to content

Commit 42702a0

Browse files
committed
feat(feeds): add batch delete all feeds in folder functionality
- Add 'Delete All Feeds in Folder' button in FeedsSetting with confirmation dialog - Fix navigation to show empty folders (persist folders even without feeds) - Localize DeleteConfirmDialog cancel/delete buttons via i18n - Add i18n keys for batch delete success/partial/failure messages - Fix fillFolderConnectors to include persisted folders with no connectors
1 parent db0db09 commit 42702a0

6 files changed

Lines changed: 137 additions & 34 deletions

File tree

app/client/src/components/DeleteConfirmDialog.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
DialogContentText,
88
DialogTitle
99
} from '@mui/material';
10+
import { useTranslation } from 'react-i18next';
1011

1112
interface DeleteConfirmDialogProps {
1213
open: boolean;
@@ -23,6 +24,8 @@ const DeleteConfirmDialog: React.FC<DeleteConfirmDialogProps> = ({
2324
onConfirm,
2425
onCancel
2526
}) => {
27+
const { t } = useTranslation(['common']);
28+
2629
return (
2730
<Dialog
2831
open={open}
@@ -38,13 +41,13 @@ const DeleteConfirmDialog: React.FC<DeleteConfirmDialogProps> = ({
3841
</DialogContent>
3942
)}
4043
<DialogActions>
41-
<Button onClick={onCancel}>Cancel</Button>
44+
<Button onClick={onCancel}>{t('common:cancel')}</Button>
4245
<Button onClick={onConfirm} autoFocus color="warning">
43-
Delete
46+
{t('common:delete')}
4447
</Button>
4548
</DialogActions>
4649
</Dialog>
4750
);
4851
};
4952

50-
export default DeleteConfirmDialog;
53+
export default DeleteConfirmDialog;

app/client/src/components/Navigation/PrimaryNavigation.tsx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -170,37 +170,37 @@ const MobileFeedsContent: React.FC<{ selectedNodeId: string }> = ({ selectedNode
170170
const items: NavTreeViewItem[] = [];
171171

172172
view.folderFeedConnectors.forEach(folder => {
173-
if (folder.connectorItems && folder.connectorItems.length > 0) {
174-
const folderInboxCount = folder.connectorItems.reduce(
173+
const connectorItems = folder.connectorItems || [];
174+
175+
if (folder.name) {
176+
const folderInboxCount = connectorItems.reduce(
175177
(sum, item) => sum + (item.inboxCount || 0),
176178
0
177179
);
178180
allInboxCount += folderInboxCount;
179181

180-
if (folder.name) {
181-
const folderItem: NavTreeViewItem = {
182-
labelText: folder.name,
183-
labelIcon: FolderOpenIcon,
184-
linkTo: `/folder/${folder.id}`,
185-
inboxCount: folderInboxCount,
186-
childItems: folder.connectorItems.map(item => ({
187-
labelText: item.name || '',
188-
labelIcon: RssFeedIcon,
189-
linkTo: `/connector/${item.id}`,
190-
inboxCount: item.inboxCount,
191-
})),
192-
};
193-
items.push(folderItem);
194-
} else {
195-
folder.connectorItems.forEach(item => {
196-
items.push({
197-
labelText: item.name || '',
198-
labelIcon: RssFeedIcon,
199-
linkTo: `/connector/${item.id}`,
200-
inboxCount: item.inboxCount,
201-
});
182+
const folderItem: NavTreeViewItem = {
183+
labelText: folder.name,
184+
labelIcon: FolderOpenIcon,
185+
linkTo: `/folder/${folder.id}`,
186+
inboxCount: folderInboxCount,
187+
childItems: connectorItems.map(item => ({
188+
labelText: item.name || '',
189+
labelIcon: RssFeedIcon,
190+
linkTo: `/connector/${item.id}`,
191+
inboxCount: item.inboxCount,
192+
})),
193+
};
194+
items.push(folderItem);
195+
} else {
196+
connectorItems.forEach(item => {
197+
items.push({
198+
labelText: item.name || '',
199+
labelIcon: RssFeedIcon,
200+
linkTo: `/connector/${item.id}`,
201+
inboxCount: item.inboxCount,
202202
});
203-
}
203+
});
204204
}
205205
});
206206

app/client/src/components/SettingModal/FeedsSetting.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder';
3535
import UploadFileIcon from '@mui/icons-material/UploadFile';
3636
import DownloadIcon from '@mui/icons-material/Download';
3737
import SettingsIcon from '@mui/icons-material/Settings';
38+
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
3839
import { styled } from "@mui/material/styles";
3940
import FolderFormDialog from "./FolderFormDialog";
4041
import FeedsFormDialog from "./FeedsFormDialog";
4142
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
4243
import { reorder } from "../../common/arrayUtils";
4344
import { useTranslation } from 'react-i18next';
45+
import DeleteConfirmDialog from "../DeleteConfirmDialog";
4446

4547
interface TabPanelProps {
4648
children?: React.ReactNode;
@@ -341,6 +343,8 @@ function FoldersTabContent() {
341343
const [folderId, setFolderId] = React.useState<number>(0);
342344
const [editFolderId, setEditFolderId] = React.useState<number>(null);
343345
const [editFeedsId, setEditFeedsId] = React.useState<number>(null);
346+
const [deleteFolderFeedsConfirmOpen, setDeleteFolderFeedsConfirmOpen] = React.useState(false);
347+
const [deletingFolderFeeds, setDeletingFolderFeeds] = React.useState(false);
344348
const { enqueueSnackbar } = useSnackbar();
345349
const {
346350
data: folders,
@@ -351,6 +355,9 @@ function FoldersTabContent() {
351355
refetch: refetchConnectors
352356
} = useQuery(["folder_connectors", folderId], async () => (await api.getSortedConnectorsByFolderIdUsingGET(folderId)).data);
353357
const queryClient = useQueryClient();
358+
const selectedFolder = folders?.find((folder) => (folder.id || 0) === folderId);
359+
const folderFeedsCount = connectors?.length || 0;
360+
const canDeleteFolderFeeds = Boolean(selectedFolder) && folderFeedsCount > 0;
354361

355362
function connectorDragEnd({ source, destination }: DropResult) {
356363
if (!destination || !source || destination.index === source.index) {
@@ -390,6 +397,61 @@ function FoldersTabContent() {
390397
});
391398
}
392399

400+
async function handleDeleteFolderFeeds() {
401+
if (!selectedFolder) {
402+
setDeleteFolderFeedsConfirmOpen(false);
403+
return;
404+
}
405+
406+
const targetFolderName = selectedFolder.name || t('settings:noFolder');
407+
const deletableConnectors = (connectors || []).filter((connector) => connector.id != null);
408+
409+
setDeleteFolderFeedsConfirmOpen(false);
410+
411+
if (deletableConnectors.length === 0) {
412+
return;
413+
}
414+
415+
setDeletingFolderFeeds(true);
416+
417+
try {
418+
const results = await Promise.allSettled(
419+
deletableConnectors.map((connector) => api.deleteFeedUsingPOST(connector.id))
420+
);
421+
const successCount = results.filter((result) => result.status === 'fulfilled').length;
422+
const failedCount = results.length - successCount;
423+
424+
if (failedCount === 0) {
425+
enqueueSnackbar(t('settings:folderFeedsDeleted', {
426+
count: successCount,
427+
name: targetFolderName
428+
}), {
429+
variant: "success",
430+
anchorOrigin: { vertical: "bottom", horizontal: "center" }
431+
});
432+
} else if (successCount > 0) {
433+
enqueueSnackbar(t('settings:folderFeedsPartiallyDeleted', {
434+
successCount,
435+
failedCount,
436+
name: targetFolderName
437+
}), {
438+
variant: "warning",
439+
anchorOrigin: { vertical: "bottom", horizontal: "center" }
440+
});
441+
} else {
442+
enqueueSnackbar(t('settings:folderFeedsDeleteFailed', {
443+
name: targetFolderName
444+
}), {
445+
variant: "error",
446+
anchorOrigin: { vertical: "bottom", horizontal: "center" }
447+
});
448+
}
449+
} finally {
450+
await Promise.all([refetchConnectors(), refetchFolders()]);
451+
setDeletingFolderFeeds(false);
452+
}
453+
}
454+
393455
return (
394456
<div className={'pb-4'}>
395457
<div className={'flex'}>
@@ -546,6 +608,20 @@ function FoldersTabContent() {
546608
</Droppable>
547609
</DragDropContext>
548610
}
611+
{
612+
canDeleteFolderFeeds && (
613+
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
614+
<Button
615+
color="warning"
616+
startIcon={<DeleteSweepIcon />}
617+
onClick={() => setDeleteFolderFeedsConfirmOpen(true)}
618+
disabled={deletingFolderFeeds || folderFeedsCount === 0}
619+
>
620+
{t('settings:deleteFolderFeeds')}
621+
</Button>
622+
</Box>
623+
)
624+
}
549625
</div>
550626
</div>
551627

@@ -561,6 +637,16 @@ function FoldersTabContent() {
561637
refetchConnectors();
562638
}} />
563639
}
640+
<DeleteConfirmDialog
641+
open={deleteFolderFeedsConfirmOpen}
642+
title={t('settings:deleteFolderFeeds')}
643+
content={t('settings:deleteFolderFeedsConfirmDesc', {
644+
count: folderFeedsCount,
645+
name: selectedFolder?.name || t('settings:noFolder')
646+
})}
647+
onConfirm={handleDeleteFolderFeeds}
648+
onCancel={() => setDeleteFolderFeedsConfirmOpen(false)}
649+
/>
564650
</div>
565651
);
566652
}

app/client/src/i18n/locales/en/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,18 @@
4444
"addFolder": "Add Folder",
4545
"editFolder": "Edit Folder",
4646
"deleteFolder": "Delete Folder",
47+
"deleteFolderFeeds": "Delete All Feeds in Folder",
4748
"folderSaved": "Folder saved.",
4849
"folderSaveFailed": "Failed to save folder.",
4950
"folderDeleted": "Folder deleted.",
5051
"folderDeleteFailed": "Failed to delete folder.",
5152
"deleteFolderConfirmDesc": "Feeds under folder \"{{name}}\" will move to the root folder. Do you want to delete it?",
53+
"deleteFolderFeedsConfirmDesc": "Articles in library will not be deleted. Do you want to delete all {{count}} feeds in folder \"{{name}}\"?",
5254
"deleteFeedConfirmDesc": "Articles in library will not be deleted. Do you want to delete this feed \"{{name}}\"?",
55+
"folderFeedsDeleted_one": "Deleted {{count}} feed from \"{{name}}\".",
56+
"folderFeedsDeleted_other": "Deleted {{count}} feeds from \"{{name}}\".",
57+
"folderFeedsPartiallyDeleted": "Deleted {{successCount}} feeds from \"{{name}}\", but {{failedCount}} failed.",
58+
"folderFeedsDeleteFailed": "Failed to delete feeds from \"{{name}}\".",
5359
"noFolder": "No Folder",
5460
"moveTo": "Move to",
5561
"githubToken": "GitHub Token",

app/client/src/i18n/locales/zh-CN/settings.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,19 @@
4444
"addFolder": "添加文件夹",
4545
"editFolder": "编辑文件夹",
4646
"deleteFolder": "删除文件夹",
47+
"deleteFolderFeeds": "删除该文件夹下全部订阅",
4748
"folderSaved": "文件夹已保存。",
4849
"folderSaveFailed": "保存文件夹失败。",
4950
"folderDeleted": "文件夹已删除。",
5051
"folderDeleteFailed": "删除文件夹失败。",
5152
"deleteFolderConfirmDesc": "文件夹“{{name}}”下的订阅将移动到根目录。确定要删除它吗?",
53+
"deleteFolderFeedsConfirmDesc": "收藏中的文章不会被删除。确定要删除文件夹“{{name}}”下全部 {{count}} 个订阅吗?",
5254
"deleteFeedConfirmDesc": "收藏中的文章不会被删除。确定要删除订阅“{{name}}”吗?",
53-
"noFolder": "无文件夹",
55+
"folderFeedsDeleted_one": "已从文件夹“{{name}}”删除 {{count}} 个订阅。",
56+
"folderFeedsDeleted_other": "已从文件夹“{{name}}”删除 {{count}} 个订阅。",
57+
"folderFeedsPartiallyDeleted": "已从文件夹“{{name}}”删除 {{successCount}} 个订阅,另有 {{failedCount}} 个删除失败。",
58+
"folderFeedsDeleteFailed": "删除文件夹“{{name}}”下的订阅失败。",
59+
"noFolder": "根文件夹",
5460
"moveTo": "移动到",
5561
"githubToken": "GitHub 令牌",
5662
"githubRepo": "仓库",
@@ -228,4 +234,4 @@
228234
"shortcutsSelected": "已选 {{count}} / {{total}}",
229235
"selectAll": "全选",
230236
"deselectAll": "取消全选"
231-
}
237+
}

app/server/huntly-server/src/main/java/com/huntly/server/service/ConnectorService.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@
1919
import org.apache.commons.lang3.StringUtils;
2020
import org.springframework.data.domain.Sort;
2121
import org.springframework.stereotype.Service;
22-
import org.springframework.util.CollectionUtils;
23-
2422
import java.time.Instant;
2523
import java.util.ArrayList;
24+
import java.util.Collections;
2625
import java.util.Comparator;
2726
import java.util.List;
2827
import java.util.Objects;
@@ -157,11 +156,14 @@ private List<FolderConnectors> getFolderFeedConnectors(List<Folder> folders, Lis
157156

158157
private void fillFolderConnectors(List<FolderConnectors> folderConnectorsList, Folder folder,
159158
List<Connector> childConnectors) {
160-
if (!CollectionUtils.isEmpty(childConnectors)) {
159+
boolean isPersistedFolder = folder.getId() != null;
160+
if (isPersistedFolder || !childConnectors.isEmpty()) {
161161
FolderConnectors folderConnectors = new FolderConnectors();
162162
folderConnectors.setId(folder.getId());
163163
folderConnectors.setName(folder.getName());
164-
folderConnectors.setConnectorItems(toConnectorItems(childConnectors));
164+
folderConnectors.setConnectorItems(childConnectors.isEmpty()
165+
? Collections.emptyList()
166+
: toConnectorItems(childConnectors));
165167
folderConnectorsList.add(folderConnectors);
166168
}
167169
}

0 commit comments

Comments
 (0)