remove listings from listingstable when clicked

This commit is contained in:
orangecoding
2025-10-03 13:27:44 +02:00
parent f97fb48e51
commit 3aa30bc1e2
5 changed files with 69 additions and 7 deletions

View File

@@ -22,10 +22,23 @@ listingsRouter.get('/table', async (req, res) => {
res.send(); res.send();
}); });
listingsRouter.delete('/', async (req, res) => { listingsRouter.delete('/job', async (req, res) => {
const { jobId } = req.body; const { jobId } = req.body;
try { try {
listingStorage.deleteListings(jobId); listingStorage.deleteListingsByJobId(jobId);
} catch (error) {
res.send(new Error(error));
logger.error(error);
}
res.send();
});
listingsRouter.delete('/', async (req, res) => {
const { ids } = req.body;
try {
if (Array.isArray(ids) && ids.length > 0) {
listingStorage.deleteListingsById(ids);
}
} catch (error) { } catch (error) {
res.send(new Error(error)); res.send(new Error(error));
logger.error(error); logger.error(error);

View File

@@ -258,7 +258,19 @@ export const queryListings = ({
* @param {string} jobId - The job identifier whose listings should be removed. * @param {string} jobId - The job identifier whose listings should be removed.
* @returns {any} The result from SqliteConnection.execute (may contain changes count). * @returns {any} The result from SqliteConnection.execute (may contain changes count).
*/ */
export const deleteListings = (jobId) => { export const deleteListingsByJobId = (jobId) => {
if (!jobId) return; if (!jobId) return;
return SqliteConnection.execute(`DELETE FROM listings WHERE job_id = @jobId`, { jobId }); return SqliteConnection.execute(`DELETE FROM listings WHERE job_id = @jobId`, { jobId });
}; };
/**
* Delete listings by a list of listing IDs.
*
* @param {string[]} ids - Array of listing IDs to delete.
* @returns {any} The result from SqliteConnection.execute.
*/
export const deleteListingsById = (ids) => {
if (!Array.isArray(ids) || ids.length === 0) return;
const placeholders = ids.map(() => '?').join(',');
return SqliteConnection.execute(`DELETE FROM listings WHERE id IN (${placeholders})`, ids);
};

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { Table, Popover, Input, Descriptions, Tag, Image, Empty } from '@douyinfe/semi-ui'; import { Table, Popover, Input, Descriptions, Tag, Image, Empty, Button, Card, Toast } from '@douyinfe/semi-ui';
import { useActions, useSelector } from '../../services/state/store.js'; import { useActions, useSelector } from '../../services/state/store.js';
import { IconClose, IconSearch, IconTick } from '@douyinfe/semi-icons'; import { IconClose, IconDelete, IconSearch, IconTick } from '@douyinfe/semi-icons';
import * as timeService from '../../services/time/timeService.js'; import * as timeService from '../../services/time/timeService.js';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import no_image from '../../assets/no_image.jpg'; import no_image from '../../assets/no_image.jpg';
@@ -9,6 +9,7 @@ import no_image from '../../assets/no_image.jpg';
import './ListingsTable.less'; 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 } from '../../services/xhr.js';
const columns = [ const columns = [
{ {
@@ -81,6 +82,7 @@ const columns = [
title: 'Title', title: 'Title',
dataIndex: 'title', dataIndex: 'title',
sorter: true, sorter: true,
ellipsis: true,
render: (text, row) => { render: (text, row) => {
return ( return (
<a href={row.url} target="_blank" rel="noopener noreferrer"> <a href={row.url} target="_blank" rel="noopener noreferrer">
@@ -106,12 +108,13 @@ export default function ListingsTable() {
const pageSize = 10; const pageSize = 10;
const [sortData, setSortData] = useState({}); const [sortData, setSortData] = useState({});
const [filter, setFilter] = useState(null); const [filter, setFilter] = useState(null);
const [selectedKeys, setSelectedKeys] = useState([]);
const handlePageChange = (_page) => { const handlePageChange = (_page) => {
setPage(_page); setPage(_page);
}; };
useEffect(() => { const loadTable = () => {
let sortfield = null; let sortfield = null;
let sortdir = null; let sortdir = null;
@@ -120,10 +123,20 @@ export default function ListingsTable() {
sortdir = sortData.direction; sortdir = sortData.direction;
} }
actions.listingsTable.getListingsTable({ page, pageSize, sortfield, sortdir, filter }); actions.listingsTable.getListingsTable({ page, pageSize, sortfield, sortdir, filter });
};
useEffect(() => {
loadTable();
}, [page, sortData, filter]); }, [page, sortData, filter]);
const handleFilterChange = useMemo(() => debounce((value) => setFilter(value), 500), []); const handleFilterChange = useMemo(() => debounce((value) => setFilter(value), 500), []);
const rowSelection = {
onChange: (selectedRowKeys) => {
setSelectedKeys(selectedRowKeys);
},
};
const expandRowRender = (record) => { const expandRowRender = (record) => {
return ( return (
<div className="listingsTable__expanded"> <div className="listingsTable__expanded">
@@ -156,6 +169,18 @@ export default function ListingsTable() {
); );
}; };
const onRemoveSelectedListings = async () => {
if (selectedKeys != null && selectedKeys.length > 0) {
try {
await xhrDelete('/api/listings/', { ids: selectedKeys });
Toast.success('Listing(s) successfully removed');
loadTable();
} catch (error) {
Toast.error(error);
}
}
};
return ( return (
<div> <div>
<Input <Input
@@ -165,12 +190,20 @@ export default function ListingsTable() {
placeholder="Search" placeholder="Search"
onChange={handleFilterChange} onChange={handleFilterChange}
/> />
{selectedKeys != null && selectedKeys.length > 0 && (
<Card className="listingsTable__toolbar">
<Button type="danger" icon={<IconDelete />} onClick={() => onRemoveSelectedListings()}>
Remove selected Listings
</Button>
</Card>
)}
<Table <Table
rowKey="id" rowKey="id"
empty={empty} empty={empty}
hideExpandedColumn={false} hideExpandedColumn={false}
sticky={{ top: 5 }} sticky={{ top: 5 }}
columns={columns} columns={columns}
rowSelection={rowSelection}
expandedRowRender={expandRowRender} expandedRowRender={expandRowRender}
dataSource={tableData?.result || []} dataSource={tableData?.result || []}
onChange={(changeSet) => { onChange={(changeSet) => {

View File

@@ -7,4 +7,8 @@
display: flex; display: flex;
gap: 1rem; gap: 1rem;
} }
&__toolbar {
margin-bottom: 1rem;
}
} }

View File

@@ -25,7 +25,7 @@ export default function Jobs() {
const onListingRemoval = async (jobId) => { const onListingRemoval = async (jobId) => {
try { try {
await xhrDelete('/api/listings', { jobId }); await xhrDelete('/api/listings/job', { jobId });
Toast.success('Listings successfully removed'); Toast.success('Listings successfully removed');
await actions.jobs.getJobs(); await actions.jobs.getJobs();
} catch (error) { } catch (error) {