{
setDeleteModalVisible(false);
diff --git a/ui/src/services/state/store.js b/ui/src/services/state/store.js
index a13ebff..e0d276a 100644
--- a/ui/src/services/state/store.js
+++ b/ui/src/services/state/store.js
@@ -349,6 +349,20 @@ export const useFredyState = create(
throw Exception;
}
},
+ async setListingDeletionPreference(listing_deletion_preference) {
+ try {
+ await xhrPost('/api/user/settings/listing-deletion-preference', { listing_deletion_preference });
+ set((state) => ({
+ userSettings: {
+ ...state.userSettings,
+ settings: { ...state.userSettings.settings, listing_deletion_preference },
+ },
+ }));
+ } catch (Exception) {
+ console.error('Error while trying to update listing deletion preference. Error:', Exception);
+ throw Exception;
+ }
+ },
},
};
diff --git a/ui/src/views/generalSettings/GeneralSettings.jsx b/ui/src/views/generalSettings/GeneralSettings.jsx
index b53fd21..9c7e451 100644
--- a/ui/src/views/generalSettings/GeneralSettings.jsx
+++ b/ui/src/views/generalSettings/GeneralSettings.jsx
@@ -18,6 +18,9 @@ import {
AutoComplete,
Select,
Banner,
+ Radio,
+ RadioGroup,
+ Typography,
} from '@douyinfe/semi-ui-19';
import { InputNumber } from '@douyinfe/semi-ui-19';
import { xhrPost, xhrGet } from '../../services/xhr';
@@ -33,6 +36,8 @@ import { debounce } from '../../utils';
import Headline from '../../components/headline/Headline.jsx';
import './GeneralSettings.less';
+const { Text } = Typography;
+
function formatFromTimestamp(ts) {
const date = new Date(ts);
return `${date.getHours()}:${date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()}`;
@@ -74,9 +79,12 @@ const GeneralSettings = function GeneralSettings() {
// User settings state
const homeAddress = useSelector((state) => state.userSettings.settings.home_address);
const providerDetails = useSelector((state) => state.userSettings.settings.provider_details);
+ const listingDeletionPreference = useSelector((state) => state.userSettings.settings.listing_deletion_preference);
const allProviders = useSelector((state) => state.provider);
const [address, setAddress] = useState(homeAddress?.address || '');
const [coords, setCoords] = useState(homeAddress?.coords || null);
+ const [listingDeleteHard, setListingDeleteHard] = useState(false);
+ const [listingDeleteSkipPrompt, setListingDeleteSkipPrompt] = useState(false);
const saving = useIsLoading(actions.userSettings.setHomeAddress);
const [dataSource, setDataSource] = useState([]);
@@ -110,6 +118,11 @@ const GeneralSettings = function GeneralSettings() {
setCoords(homeAddress?.coords || null);
}, [homeAddress]);
+ useEffect(() => {
+ setListingDeleteHard(listingDeletionPreference?.hardDelete ?? false);
+ setListingDeleteSkipPrompt(listingDeletionPreference?.skipPrompt ?? false);
+ }, [listingDeletionPreference]);
+
const nullOrEmpty = (val) => val == null || val.length === 0;
const handleStore = async () => {
@@ -218,6 +231,10 @@ const GeneralSettings = function GeneralSettings() {
try {
const responseJson = await actions.userSettings.setHomeAddress(address);
setCoords(responseJson.coords);
+ await actions.userSettings.setListingDeletionPreference({
+ skipPrompt: listingDeleteSkipPrompt,
+ hardDelete: listingDeleteHard,
+ });
await actions.userSettings.getUserSettings();
Toast.success('Settings saved. Distance calculations are running in the background.');
} catch (error) {
@@ -459,6 +476,48 @@ const GeneralSettings = function GeneralSettings() {
/>
+
+ setListingDeleteHard(e.target.value === 'hard')}
+ >
+
+
+ Mark as deleted (Soft Delete)
+
+
+ Listings are kept in the database but marked as hidden. They will not re-appear during
+ the next scraping session.
+
+
+
+
+
+ Remove from database (Hard Delete)
+
+
+ Listings are completely removed from the database.
+
+
+ Consequence: They might re-appear when scraping the next time because Fredy won't know they
+ were previously found.
+
+
+
+
+
+ setListingDeleteSkipPrompt(e.target.checked)}
+ style={{ marginTop: 12 }}
+ >
+ Skip confirmation dialog
+
+
+
}
diff --git a/ui/src/views/listings/ListingDetail.jsx b/ui/src/views/listings/ListingDetail.jsx
index 15372ae..7cc9412 100644
--- a/ui/src/views/listings/ListingDetail.jsx
+++ b/ui/src/views/listings/ListingDetail.jsx
@@ -57,7 +57,10 @@ export default function ListingDetail() {
const navigate = useNavigate();
const actions = useActions();
const listing = useSelector((state) => state.listingsData.currentListing);
- const homeAddress = useSelector((state) => state.userSettings.settings.home_address);
+ const userSettings = useSelector((state) => state.userSettings.settings);
+ const homeAddress = userSettings?.home_address;
+ const listingDeletionPref = userSettings?.listing_deletion_preference;
+ const defaultDeleteType = listingDeletionPref?.hardDelete ? 'hard' : 'soft';
const mapContainer = useRef(null);
const map = useRef(null);
const [loading, setLoading] = useState(true);
@@ -242,8 +245,11 @@ export default function ListingDetail() {
};
}, [listing, loading, homeAddress]);
- const confirmDeletion = async (hardDelete) => {
+ const confirmDeletion = async (hardDelete, remember) => {
try {
+ if (remember) {
+ await actions.userSettings.setListingDeletionPreference({ skipPrompt: true, hardDelete });
+ }
await xhrDelete('/api/listings/', { ids: [listing.id], hardDelete });
Toast.success('Listing successfully removed');
navigate('/listings');
@@ -347,7 +353,13 @@ export default function ListingDetail() {
}
- onClick={() => setDeleteModalVisible(true)}
+ onClick={() => {
+ if (listingDeletionPref?.skipPrompt) {
+ confirmDeletion(listingDeletionPref.hardDelete);
+ return;
+ }
+ setDeleteModalVisible(true);
+ }}
theme="light"
type="danger"
>
@@ -423,6 +435,7 @@ export default function ListingDetail() {
setDeleteModalVisible(false)}
/>
diff --git a/ui/src/views/listings/Map.jsx b/ui/src/views/listings/Map.jsx
index 9d663ec..a7c81f5 100644
--- a/ui/src/views/listings/Map.jsx
+++ b/ui/src/views/listings/Map.jsx
@@ -37,7 +37,10 @@ export default function MapView() {
const sp = useSearchParams();
const [searchParams, setSearchParams] = sp;
const listings = useSelector((state) => state.listingsData.mapListings);
- const homeAddress = useSelector((state) => state.userSettings.settings.home_address);
+ const userSettings = useSelector((state) => state.userSettings.settings);
+ const homeAddress = userSettings?.home_address;
+ const listingDeletionPref = userSettings?.listing_deletion_preference;
+ const defaultDeleteType = listingDeletionPref?.hardDelete ? 'hard' : 'soft';
const jobs = useSelector((state) => state.jobsData.jobs);
const [jobId, setJobId] = useSearchParamState(sp, 'job', null, parseString);
@@ -52,10 +55,14 @@ export default function MapView() {
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [listingToDelete, setListingToDelete] = useState(null);
+ const deleteListingRef = useRef(null);
- const confirmListingDeletion = async (hardDelete) => {
+ const confirmListingDeletion = 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');
fetchListings();
} catch (error) {
@@ -66,6 +73,15 @@ export default function MapView() {
}
};
+ deleteListingRef.current = (id) => {
+ if (listingDeletionPref?.skipPrompt) {
+ confirmListingDeletion(listingDeletionPref.hardDelete, false, id);
+ return;
+ }
+ setListingToDelete(id);
+ setDeleteModalVisible(true);
+ };
+
useEffect(() => {
// Only reset to full range when no URL override is set
if (urlPriceMax === null) {
@@ -88,10 +104,7 @@ export default function MapView() {
};
useEffect(() => {
- window.deleteListing = (id) => {
- setListingToDelete(id);
- setDeleteModalVisible(true);
- };
+ window.deleteListing = (id) => deleteListingRef.current(id);
window.viewDetails = (id) => {
navigate(`/listings/listing/${id}`);
@@ -472,6 +485,7 @@ export default function MapView() {
{
setDeleteModalVisible(false);