/* * Copyright (c) 2026 by Christian Kellner. * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause */ import { useState, useEffect, useMemo } from 'react'; import { useSearchParamState, parseNumber, parseString, parseNullableBoolean, } from '../../hooks/useSearchParamState.js'; import { Button, Pagination, Toast, Input, Select, Empty, Radio, RadioGroup, Tooltip } from '@douyinfe/semi-ui-19'; import { IconSearch, IconArrowUp, IconArrowDown, IconGridView, IconList } from '@douyinfe/semi-icons'; import { useNavigate, useSearchParams } from 'react-router-dom'; import ListingDeletionModal from '../ListingDeletionModal.jsx'; import { xhrDelete, xhrPost } from '../../services/xhr.js'; import { useActions, useSelector } from '../../services/state/store.js'; import { debounce } from '../../utils'; import ListingsGrid from '../grid/listings/ListingsGrid.jsx'; import ListingsTable from '../table/ListingsTable.jsx'; import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; import './ListingsOverview.less'; const ListingsOverview = ({ mode = 'all' }) => { const isWatchlistMode = mode === 'watchlist'; const listingsData = useSelector((state) => state.listingsData); const providers = useSelector((state) => state.provider); const jobs = useSelector((state) => state.jobsData.jobs); const userSettings = useSelector((state) => state.userSettings.settings); const actions = useActions(); const navigate = useNavigate(); 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; const [sortField, setSortField] = useSearchParamState(sp, 'sort', 'created_at', parseString); const [sortDir, setSortDir] = useSearchParamState(sp, 'dir', 'desc', parseString); const [freeTextFilter, setFreeTextFilter] = useSearchParamState(sp, 'q', null, parseString); const [watchListFilter, setWatchListFilter] = useSearchParamState(sp, 'watch', null, parseNullableBoolean); const [jobNameFilter, setJobNameFilter] = useSearchParamState(sp, 'job', null, parseString); const [activityFilter, setActivityFilter] = useSearchParamState(sp, 'active', null, parseNullableBoolean); const [providerFilter, setProviderFilter] = useSearchParamState(sp, 'provider', null, parseString); const [statusFilter, setStatusFilter] = useSearchParamState(sp, 'status', null, parseString); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [listingToDelete, setListingToDelete] = useState(null); // In watchlist mode the watch filter is forced to "watched only" — regardless of the URL. const effectiveWatchListFilter = isWatchlistMode ? true : watchListFilter; const loadData = () => { actions.listingsData.getListingsData({ page, pageSize, sortfield: sortField, sortdir: sortDir, freeTextFilter, filter: { watchListFilter: effectiveWatchListFilter, jobNameFilter, activityFilter, providerFilter, statusFilter, }, }); }; useEffect(() => { loadData(); }, [ page, sortField, sortDir, freeTextFilter, providerFilter, activityFilter, jobNameFilter, watchListFilter, statusFilter, isWatchlistMode, ]); const handleFilterChange = useMemo( () => debounce((value) => { setFreeTextFilter(value || null); setPage(1); }, 500), [], ); useEffect(() => { return () => { handleFilterChange.cancel && handleFilterChange.cancel(); }; }, [handleFilterChange]); const handleWatch = async (e, item) => { e.preventDefault(); e.stopPropagation(); try { await xhrPost('/api/listings/watch', { listingId: item.id }); Toast.success(item.isWatched === 1 ? 'Listing removed from Watchlist' : 'Listing added to Watchlist'); loadData(); } catch (e) { console.error(e); Toast.error('Failed to operate Watchlist'); } }; const handleStatusChange = async (item, nextStatus) => { try { await actions.listingsData.setListingStatus(item.id, nextStatus); Toast.success(nextStatus ? `Marked as ${nextStatus}` : 'Status cleared'); loadData(); } catch (e) { console.error(e); Toast.error('Failed to update status'); } }; 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, remember, id = listingToDelete) => { try { if (remember) { await actions.userSettings.setListingDeletionPreference({ skipPrompt: true, hardDelete }); } await xhrDelete('/api/listings/', { ids: [id], hardDelete }); Toast.success('Listing successfully removed'); loadData(); } catch (error) { Toast.error(error.message || 'Error deleting listing'); } finally { setDeleteModalVisible(false); setListingToDelete(null); } }; const listings = listingsData?.result || []; return (