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

@@ -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() {
/>
</SegmentPart>
<SegmentPart
name="Listing deletion"
helpText="Choose the default deletion mode. Soft delete hides them without re-scraping; hard delete removes them from the database."
>
<RadioGroup
value={listingDeleteHard ? 'hard' : 'soft'}
onChange={(e) => setListingDeleteHard(e.target.value === 'hard')}
>
<Radio value="soft">
<div>
<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">
<div>
<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={listingDeleteSkipPrompt}
onChange={(e) => setListingDeleteSkipPrompt(e.target.checked)}
style={{ marginTop: 12 }}
>
Skip confirmation dialog
</Checkbox>
</SegmentPart>
<div className="generalSettings__save-row">
<Button
icon={<IconSave />}

View File

@@ -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() {
</a>
<Button
icon={<IconDelete />}
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() {
<ListingDeletionModal
visible={deleteModalVisible}
defaultDeleteType={defaultDeleteType}
onConfirm={confirmDeletion}
onCancel={() => setDeleteModalVisible(false)}
/>

View File

@@ -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() {
<ListingDeletionModal
visible={deleteModalVisible}
defaultDeleteType={defaultDeleteType}
onConfirm={confirmListingDeletion}
onCancel={() => {
setDeleteModalVisible(false);