feat: remember listing delete preference (#314)

* feat: remember listing delete preference

Persist soft/hard choice and skip-confirm in user settings.
This commit is contained in:
Ramin
2026-06-02 10:23:45 +02:00
committed by GitHub
parent b56e13aa16
commit f1b8709ab7
9 changed files with 206 additions and 42 deletions

View File

@@ -3,8 +3,8 @@
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
import { useState } from 'react';
import { Modal, Radio, RadioGroup, Typography } from '@douyinfe/semi-ui-19';
import { useState, useEffect } from 'react';
import { Modal, Radio, RadioGroup, Typography, Checkbox } from '@douyinfe/semi-ui-19';
const { Text } = Typography;
@@ -15,11 +15,24 @@ const ListingDeletionModal = ({
title = 'Delete Listings',
showOptions = true,
message = 'How would you like to delete the selected listing(s)?',
defaultDeleteType = 'soft',
}) => {
const [deleteType, setDeleteType] = useState('soft');
const [remember, setRemember] = useState(false);
useEffect(() => {
if (visible) {
setDeleteType(defaultDeleteType);
setRemember(false);
}
}, [visible, defaultDeleteType]);
const handleOk = () => {
onConfirm(!showOptions || deleteType === 'hard');
if (showOptions) {
onConfirm(deleteType === 'hard', remember);
} else {
onConfirm(true);
}
};
return (
@@ -36,32 +49,37 @@ const ListingDeletionModal = ({
<Text>{message}</Text>
</div>
{showOptions && (
<RadioGroup value={deleteType} onChange={(e) => setDeleteType(e.target.value)} style={{ width: '100%' }}>
<Radio value="soft" style={{ alignItems: 'flex-start', width: '100%' }}>
<div style={{ marginLeft: 8 }}>
<Text strong>Mark as deleted (Soft Delete)</Text>
<br />
<Text type="secondary">
Listings are kept in the database but marked as hidden. They will <b>not</b> re-appear during the next
scraping session.
</Text>
</div>
</Radio>
<Radio value="hard" style={{ marginTop: 16, alignItems: 'flex-start', width: '100%' }}>
<div style={{ marginLeft: 8 }}>
<Text strong>Remove from database (Hard Delete)</Text>
<br />
<Text type="secondary">
Listings are completely removed from the database.
<>
<RadioGroup value={deleteType} onChange={(e) => setDeleteType(e.target.value)} style={{ width: '100%' }}>
<Radio value="soft" style={{ alignItems: 'flex-start', width: '100%' }}>
<div style={{ marginLeft: 8 }}>
<Text strong>Mark as deleted (Soft Delete)</Text>
<br />
<Text type="warning">
Consequence: They might re-appear when scraping the next time because Fredy won't know they were
previously found.
<Text type="secondary">
Listings are kept in the database but marked as hidden. They will <b>not</b> re-appear during the next
scraping session.
</Text>
</Text>
</div>
</Radio>
</RadioGroup>
</div>
</Radio>
<Radio value="hard" style={{ marginTop: 16, alignItems: 'flex-start', width: '100%' }}>
<div style={{ marginLeft: 8 }}>
<Text strong>Remove from database (Hard Delete)</Text>
<br />
<Text type="secondary">
Listings are completely removed from the database.
<br />
<Text type="warning">
Consequence: They might re-appear when scraping the next time because Fredy won't know they were
previously found.
</Text>
</Text>
</div>
</Radio>
</RadioGroup>
<Checkbox checked={remember} onChange={(e) => setRemember(e.target.checked)} style={{ marginTop: 16 }}>
Remember my choice and skip this dialog next time
</Checkbox>
</>
)}
</Modal>
);

View File

@@ -60,6 +60,8 @@ const JobGrid = () => {
const userSettings = useSelector((state) => state.userSettings.settings);
const viewMode = userSettings?.jobs_view_mode ?? 'grid';
const listingDeletionPref = userSettings?.listing_deletion_preference;
const defaultDeleteType = listingDeletionPref?.hardDelete ? 'hard' : 'soft';
const [page, setPage] = useState(1);
const pageSize = 12;
@@ -142,13 +144,21 @@ const JobGrid = () => {
};
const onListingRemoval = (jobId) => {
setPendingDeletion({ type: 'listings', jobId });
const deletion = { type: 'listings', jobId };
if (listingDeletionPref?.skipPrompt) {
confirmDeletion(listingDeletionPref.hardDelete, false, deletion);
return;
}
setPendingDeletion(deletion);
setDeleteModalVisible(true);
};
const confirmDeletion = async (hardDelete) => {
const { type, jobId } = pendingDeletion;
const confirmDeletion = async (hardDelete, remember, deletion = pendingDeletion) => {
const { type, jobId } = deletion;
try {
if (remember && type === 'listings') {
await actions.userSettings.setListingDeletionPreference({ skipPrompt: true, hardDelete });
}
if (type === 'job') {
await xhrDelete('/api/jobs', { jobId });
Toast.success('Job and listings successfully removed');
@@ -425,6 +435,7 @@ const JobGrid = () => {
visible={deleteModalVisible}
title={pendingDeletion?.type === 'job' ? 'Delete Job' : 'Delete Listings'}
showOptions={pendingDeletion?.type !== 'job'}
defaultDeleteType={defaultDeleteType}
message={
pendingDeletion?.type === 'job'
? 'Are you sure you want to delete this job? All associated listings will be removed from the database.'

View File

@@ -33,6 +33,8 @@ const ListingsOverview = () => {
const sp = useSearchParams();
const viewMode = userSettings?.listings_view_mode ?? 'grid';
const listingDeletionPref = userSettings?.listing_deletion_preference;
const defaultDeleteType = listingDeletionPref?.hardDelete ? 'hard' : 'soft';
const [page, setPage] = useSearchParamState(sp, 'page', 1, parseNumber);
const pageSize = 40;
@@ -91,15 +93,22 @@ const ListingsOverview = () => {
};
const handleDelete = (id) => {
if (listingDeletionPref?.skipPrompt) {
confirmDeletion(listingDeletionPref.hardDelete, false, id);
return;
}
setListingToDelete(id);
setDeleteModalVisible(true);
};
const handleNavigate = (id) => navigate(`/listings/listing/${id}`);
const confirmDeletion = async (hardDelete) => {
const confirmDeletion = async (hardDelete, remember, id = listingToDelete) => {
try {
await xhrDelete('/api/listings/', { ids: [listingToDelete], hardDelete });
if (remember) {
await actions.userSettings.setListingDeletionPreference({ skipPrompt: true, hardDelete });
}
await xhrDelete('/api/listings/', { ids: [id], hardDelete });
Toast.success('Listing successfully removed');
loadData();
} catch (error) {
@@ -251,6 +260,7 @@ const ListingsOverview = () => {
<ListingDeletionModal
visible={deleteModalVisible}
defaultDeleteType={defaultDeleteType}
onConfirm={confirmDeletion}
onCancel={() => {
setDeleteModalVisible(false);