diff --git a/ui/src/components/table/listings/ListingsFilter.jsx b/ui/src/components/table/listings/ListingsFilter.jsx
deleted file mode 100644
index 77004e7..0000000
--- a/ui/src/components/table/listings/ListingsFilter.jsx
+++ /dev/null
@@ -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 (
-
- Filter by:
-
-
-
-
- onWatchListFilter(e.target.checked)}>Only Watch List
-
-
- onActivityFilter(e.target.checked)}>Only Active Listings
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/ui/src/components/table/listings/ListingsFilter.less b/ui/src/components/table/listings/ListingsFilter.less
deleted file mode 100644
index a3a1d23..0000000
--- a/ui/src/components/table/listings/ListingsFilter.less
+++ /dev/null
@@ -1,4 +0,0 @@
-.listingsFilter {
- margin-bottom: 1rem;
- background: rgb(53, 54, 60);
-}
\ No newline at end of file
diff --git a/ui/src/components/table/listings/ListingsTable.jsx b/ui/src/components/table/listings/ListingsTable.jsx
index 4eecbee..d630fec 100644
--- a/ui/src/components/table/listings/ListingsTable.jsx
+++ b/ui/src/components/table/listings/ListingsTable.jsx
@@ -1,5 +1,18 @@
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 { IconClose, IconDelete, IconSearch, IconStar, IconStarStroked, IconTick } from '@douyinfe/semi-icons';
import * as timeService from '../../../services/time/timeService.js';
@@ -10,166 +23,220 @@ import './ListingsTable.less';
import { format } from '../../../services/time/timeService.js';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
import { xhrDelete, xhrPost } from '../../../services/xhr.js';
-import ListingsFilter from './ListingsFilter.jsx';
-const columns = [
- {
- title: 'Watchlist',
- width: 110,
- dataIndex: 'isWatched',
- sorter: true,
- render: (id, row) => {
- return (
-
-
-
- ) : (
-
- )
- }
- theme="borderless"
- size="small"
- onClick={async () => {
- try {
- await xhrPost('/api/listings/watch', { listingId: row.id });
- Toast.success(row.isWatched === 1 ? 'Listing removed from Watchlist' : 'Listing added to Watchlist');
- row.reloadTable();
- } catch (e) {
- console.error(e);
- Toast.error('Failed to operate Watchlist');
- }
+const getColumns = (provider, setProviderFilter, jobs, setJobNameFilter) => {
+ return [
+ {
+ title: 'Watchlist',
+ width: 133,
+ dataIndex: 'isWatched',
+ sorter: true,
+ filters: [
+ {
+ text: 'Show only watched listings',
+ value: 'watchList',
+ },
+ ],
+ render: (id, row) => {
+ return (
+
+
-
-
-
- }
- theme="borderless"
- size="small"
- type="danger"
- onClick={async () => {
- try {
- await xhrDelete('/api/listings/', { ids: [row.id] });
- Toast.success('Listing(s) successfully removed');
- row.reloadTable();
- } catch (error) {
- Toast.error(error);
+ content={row.isWatched === 1 ? 'Unwatch Listing' : 'Watch Listing'}
+ >
+
+ ) : (
+
+ )
}
+ theme="borderless"
+ size="small"
+ onClick={async () => {
+ try {
+ await xhrPost('/api/listings/watch', { listingId: row.id });
+ Toast.success(
+ row.isWatched === 1 ? 'Listing removed from Watchlist' : 'Listing added to Watchlist',
+ );
+ row.reloadTable();
+ } catch (e) {
+ console.error(e);
+ Toast.error('Failed to operate Watchlist');
+ }
+ }}
+ />
+
+
+
-
-
- );
+ content="Delete Listing"
+ >
+ }
+ theme="borderless"
+ size="small"
+ type="danger"
+ onClick={async () => {
+ try {
+ await xhrDelete('/api/listings/', { ids: [row.id] });
+ Toast.success('Listing(s) successfully removed');
+ row.reloadTable();
+ } catch (error) {
+ Toast.error(error);
+ }
+ }}
+ />
+
+
+ );
+ },
},
- },
- {
- title: 'State',
- dataIndex: 'is_active',
- width: 84,
- sorter: true,
- render: (value) => {
- return value ? (
-
- ) : (
-
- );
+ {
+ title: 'State',
+ dataIndex: 'is_active',
+ width: 105,
+ sorter: true,
+ filters: [
+ {
+ text: 'Show only active listings',
+ value: 'activityStatus',
+ },
+ ],
+ render: (value) => {
+ return value ? (
+
+ ) : (
+
+ );
+ },
},
- },
- {
- title: 'Job-Name',
- sorter: true,
- ellipsis: true,
- dataIndex: 'job_name',
- width: 150,
- },
- {
- title: 'Listing date',
- width: 130,
- dataIndex: 'created_at',
- sorter: true,
- render: (text) => timeService.format(text, false),
- },
- {
- title: 'Provider',
- width: 130,
- dataIndex: 'provider',
- sorter: true,
- render: (text) => text.charAt(0).toUpperCase() + text.slice(1),
- },
- {
- title: 'Price',
- width: 110,
- dataIndex: 'price',
- sorter: true,
- render: (text) => text + ' €',
- },
- {
- title: 'Address',
- width: 150,
- dataIndex: 'address',
- sorter: true,
- },
- {
- title: 'Title',
- dataIndex: 'title',
- sorter: true,
- ellipsis: true,
- render: (text, row) => {
- return (
-
- {text}
-
- );
+ {
+ title: 'Job-Name',
+ sorter: true,
+ ellipsis: true,
+ dataIndex: 'job_name',
+ width: 150,
+ onFilter: () => true,
+ renderFilterDropdown: () => {
+ return (
+
+
+
+ );
+ },
},
- },
-];
+ {
+ title: 'Listing date',
+ width: 130,
+ dataIndex: 'created_at',
+ sorter: true,
+ render: (text) => timeService.format(text, false),
+ },
+ {
+ title: 'Provider',
+ width: 130,
+ dataIndex: 'provider',
+ sorter: true,
+ render: (text) => text.charAt(0).toUpperCase() + text.slice(1),
+ onFilter: () => true,
+ renderFilterDropdown: () => {
+ return (
+
+
+
+ );
+ },
+ },
+ {
+ title: 'Price',
+ width: 110,
+ dataIndex: 'price',
+ sorter: true,
+ render: (text) => text + ' €',
+ },
+ {
+ title: 'Address',
+ width: 150,
+ dataIndex: 'address',
+ sorter: true,
+ },
+ {
+ title: 'Title',
+ dataIndex: 'title',
+ sorter: true,
+ ellipsis: true,
+ render: (text, row) => {
+ return (
+
+ {text}
+
+ );
+ },
+ },
+ ];
+};
const empty = (
}
darkModeImage={}
- description="No listings available."
+ description="No listings found."
/>
);
export default function ListingsTable() {
const tableData = useSelector((state) => state.listingsTable);
+ const provider = useSelector((state) => state.provider);
+ const jobs = useSelector((state) => state.jobs.jobs);
+
const actions = useActions();
const [page, setPage] = useState(1);
const pageSize = 10;
@@ -179,12 +246,14 @@ export default function ListingsTable() {
const [jobNameFilter, setJobNameFilter] = useState(null);
const [activityFilter, setActivityFilter] = useState(null);
const [providerFilter, setProviderFilter] = useState(null);
+ const [allFilters, setAllFilters] = useState([]);
const [imageWidth, setImageWidth] = useState('100%');
const handlePageChange = (_page) => {
setPage(_page);
};
+ const columns = getColumns(provider, setProviderFilter, jobs, setJobNameFilter);
const loadTable = () => {
let sortfield = null;
let sortdir = null;
@@ -209,6 +278,20 @@ export default function ListingsTable() {
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(() => {
return () => {
// cleanup debounced handler to avoid memory leaks
@@ -258,12 +341,6 @@ export default function ListingsTable() {
return (
-
}
showClear
@@ -285,7 +362,23 @@ export default function ListingsTable() {
};
})}
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({
field: changeSet.sorter.dataIndex,
direction: changeSet.sorter.sortOrder === 'ascend' ? 'asc' : 'desc',