improve filtering for listings

This commit is contained in:
orangecoding
2025-11-01 16:29:05 +01:00
parent 60525d4cca
commit 96cd9098f0
3 changed files with 244 additions and 208 deletions

View File

@@ -1,53 +0,0 @@
import { Card, Checkbox, Descriptions, Divider, Select } from '@douyinfe/semi-ui';
import React from 'react';
import { useSelector } from '../../../services/state/store.js';
import { Typography } from '@douyinfe/semi-ui';
import './ListingsFilter.less';
export default function ListingsFilter({ onWatchListFilter, onActivityFilter, onJobNameFilter, onProviderFilter }) {
const jobs = useSelector((state) => state.jobs.jobs);
const provider = useSelector((state) => state.provider);
const { Title } = Typography;
return (
<Card className="listingsFilter">
<Title heading={6}>Filter by:</Title>
<Divider />
<br />
<Descriptions row>
<Descriptions.Item itemKey="Watch List">
<Checkbox onChange={(e) => onWatchListFilter(e.target.checked)}>Only Watch List</Checkbox>
</Descriptions.Item>
<Descriptions.Item itemKey="Activity status">
<Checkbox onChange={(e) => onActivityFilter(e.target.checked)}>Only Active Listings</Checkbox>
</Descriptions.Item>
<Descriptions.Item itemKey="Job Name">
<Select showClear placeholder="Select Job to Filter" onChange={(val) => onJobNameFilter(val)}>
{jobs != null &&
jobs.length > 0 &&
jobs.map((job) => {
return (
<Select.Option value={job.id} key={job.id}>
{job.name}
</Select.Option>
);
})}
</Select>
</Descriptions.Item>
<Descriptions.Item itemKey="Provider">
<Select showClear placeholder="Select Provider to Filter" onChange={(val) => onProviderFilter(val)}>
{provider != null &&
provider.length > 0 &&
provider.map((prov) => {
return (
<Select.Option value={prov.id} key={prov.id}>
{prov.name}
</Select.Option>
);
})}
</Select>
</Descriptions.Item>
</Descriptions>
</Card>
);
}

View File

@@ -1,4 +0,0 @@
.listingsFilter {
margin-bottom: 1rem;
background: rgb(53, 54, 60);
}

View File

@@ -1,5 +1,18 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { Table, Popover, Input, Descriptions, Tag, Image, Empty, Button, Toast, Divider } from '@douyinfe/semi-ui'; import {
Table,
Popover,
Input,
Descriptions,
Tag,
Image,
Empty,
Button,
Toast,
Divider,
Space,
Select,
} from '@douyinfe/semi-ui';
import { useActions, useSelector } from '../../../services/state/store.js'; import { useActions, useSelector } from '../../../services/state/store.js';
import { IconClose, IconDelete, IconSearch, IconStar, IconStarStroked, IconTick } from '@douyinfe/semi-icons'; import { IconClose, IconDelete, IconSearch, IconStar, IconStarStroked, IconTick } from '@douyinfe/semi-icons';
import * as timeService from '../../../services/time/timeService.js'; import * as timeService from '../../../services/time/timeService.js';
@@ -10,14 +23,20 @@ import './ListingsTable.less';
import { format } from '../../../services/time/timeService.js'; import { format } from '../../../services/time/timeService.js';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
import { xhrDelete, xhrPost } from '../../../services/xhr.js'; import { xhrDelete, xhrPost } from '../../../services/xhr.js';
import ListingsFilter from './ListingsFilter.jsx';
const columns = [ const getColumns = (provider, setProviderFilter, jobs, setJobNameFilter) => {
return [
{ {
title: 'Watchlist', title: 'Watchlist',
width: 110, width: 133,
dataIndex: 'isWatched', dataIndex: 'isWatched',
sorter: true, sorter: true,
filters: [
{
text: 'Show only watched listings',
value: 'watchList',
},
],
render: (id, row) => { render: (id, row) => {
return ( return (
<div> <div>
@@ -41,7 +60,9 @@ const columns = [
onClick={async () => { onClick={async () => {
try { try {
await xhrPost('/api/listings/watch', { listingId: row.id }); await xhrPost('/api/listings/watch', { listingId: row.id });
Toast.success(row.isWatched === 1 ? 'Listing removed from Watchlist' : 'Listing added to Watchlist'); Toast.success(
row.isWatched === 1 ? 'Listing removed from Watchlist' : 'Listing added to Watchlist',
);
row.reloadTable(); row.reloadTable();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -81,8 +102,14 @@ const columns = [
{ {
title: 'State', title: 'State',
dataIndex: 'is_active', dataIndex: 'is_active',
width: 84, width: 105,
sorter: true, sorter: true,
filters: [
{
text: 'Show only active listings',
value: 'activityStatus',
},
],
render: (value) => { render: (value) => {
return value ? ( return value ? (
<div style={{ color: 'rgba(var(--semi-green-6), 1)' }}> <div style={{ color: 'rgba(var(--semi-green-6), 1)' }}>
@@ -117,6 +144,24 @@ const columns = [
ellipsis: true, ellipsis: true,
dataIndex: 'job_name', dataIndex: 'job_name',
width: 150, width: 150,
onFilter: () => true,
renderFilterDropdown: () => {
return (
<Space vertical style={{ padding: 8 }}>
<Select showClear placeholder="Select Job to Filter" onChange={(val) => setJobNameFilter(val)}>
{jobs != null &&
jobs.length > 0 &&
jobs.map((job) => {
return (
<Select.Option value={job.id} key={job.id}>
{job.name}
</Select.Option>
);
})}
</Select>
</Space>
);
},
}, },
{ {
title: 'Listing date', title: 'Listing date',
@@ -131,6 +176,24 @@ const columns = [
dataIndex: 'provider', dataIndex: 'provider',
sorter: true, sorter: true,
render: (text) => text.charAt(0).toUpperCase() + text.slice(1), render: (text) => text.charAt(0).toUpperCase() + text.slice(1),
onFilter: () => true,
renderFilterDropdown: () => {
return (
<Space vertical style={{ padding: 8 }}>
<Select showClear placeholder="Select Provider to Filter" onChange={(val) => setProviderFilter(val)}>
{provider != null &&
provider.length > 0 &&
provider.map((prov) => {
return (
<Select.Option value={prov.id} key={prov.id}>
{prov.name}
</Select.Option>
);
})}
</Select>
</Space>
);
},
}, },
{ {
title: 'Price', title: 'Price',
@@ -158,18 +221,22 @@ const columns = [
); );
}, },
}, },
]; ];
};
const empty = ( const empty = (
<Empty <Empty
image={<IllustrationNoResult />} image={<IllustrationNoResult />}
darkModeImage={<IllustrationNoResultDark />} darkModeImage={<IllustrationNoResultDark />}
description="No listings available." description="No listings found."
/> />
); );
export default function ListingsTable() { export default function ListingsTable() {
const tableData = useSelector((state) => state.listingsTable); const tableData = useSelector((state) => state.listingsTable);
const provider = useSelector((state) => state.provider);
const jobs = useSelector((state) => state.jobs.jobs);
const actions = useActions(); const actions = useActions();
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const pageSize = 10; const pageSize = 10;
@@ -179,12 +246,14 @@ export default function ListingsTable() {
const [jobNameFilter, setJobNameFilter] = useState(null); const [jobNameFilter, setJobNameFilter] = useState(null);
const [activityFilter, setActivityFilter] = useState(null); const [activityFilter, setActivityFilter] = useState(null);
const [providerFilter, setProviderFilter] = useState(null); const [providerFilter, setProviderFilter] = useState(null);
const [allFilters, setAllFilters] = useState([]);
const [imageWidth, setImageWidth] = useState('100%'); const [imageWidth, setImageWidth] = useState('100%');
const handlePageChange = (_page) => { const handlePageChange = (_page) => {
setPage(_page); setPage(_page);
}; };
const columns = getColumns(provider, setProviderFilter, jobs, setJobNameFilter);
const loadTable = () => { const loadTable = () => {
let sortfield = null; let sortfield = null;
let sortdir = null; let sortdir = null;
@@ -209,6 +278,20 @@ export default function ListingsTable() {
const handleFilterChange = useMemo(() => debounce((value) => setFreeTextFilter(value), 500), []); const handleFilterChange = useMemo(() => debounce((value) => setFreeTextFilter(value), 500), []);
const diffArrays = (primary, secondary) => {
const result = {};
for (const item of secondary) {
if (!primary.includes(item)) result[item] = true;
}
for (const item of primary) {
if (!secondary.includes(item)) result[item] = false;
}
return [result];
};
useEffect(() => { useEffect(() => {
return () => { return () => {
// cleanup debounced handler to avoid memory leaks // cleanup debounced handler to avoid memory leaks
@@ -258,12 +341,6 @@ export default function ListingsTable() {
return ( return (
<div> <div>
<ListingsFilter
onActivityFilter={setActivityFilter}
onWatchListFilter={setWatchListFilter}
onJobNameFilter={setJobNameFilter}
onProviderFilter={setProviderFilter}
/>
<Input <Input
prefix={<IconSearch />} prefix={<IconSearch />}
showClear showClear
@@ -285,7 +362,23 @@ export default function ListingsTable() {
}; };
})} })}
onChange={(changeSet) => { onChange={(changeSet) => {
if (changeSet?.extra?.changeType === 'sorter') { if (changeSet?.extra?.changeType === 'filter') {
const transformed = changeSet.filters.map((f) => f.dataIndex);
const diff = diffArrays(allFilters, transformed);
setAllFilters(transformed);
diff.forEach((filter) => {
switch (Object.keys(filter)[0]) {
case 'isWatched':
setWatchListFilter(Object.values(filter)[0]);
break;
case 'is_active':
setActivityFilter(Object.values(filter)[0]);
break;
default:
console.error('Unknown filter: ', filter.dataIndex);
}
});
} else if (changeSet?.extra?.changeType === 'sorter') {
setSortData({ setSortData({
field: changeSet.sorter.dataIndex, field: changeSet.sorter.dataIndex,
direction: changeSet.sorter.sortOrder === 'ascend' ? 'asc' : 'desc', direction: changeSet.sorter.sortOrder === 'ascend' ? 'asc' : 'desc',