mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f00966f27 | ||
|
|
921057252d | ||
|
|
703c602527 |
@@ -126,4 +126,21 @@ export default async function userSettingsPlugin(fastify) {
|
|||||||
return reply.code(500).send({ error: error.message });
|
return reply.code(500).send({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fastify.post('/jobs-view-mode', async (request, reply) => {
|
||||||
|
const userId = request.session.currentUser;
|
||||||
|
const { jobs_view_mode } = request.body;
|
||||||
|
|
||||||
|
if (jobs_view_mode !== 'grid' && jobs_view_mode !== 'table') {
|
||||||
|
return reply.code(400).send({ error: 'jobs_view_mode must be "grid" or "table".' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
upsertSettings({ jobs_view_mode }, userId);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error updating jobs view mode setting', error);
|
||||||
|
return reply.code(500).send({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,10 +168,6 @@ export function convertWebToMobile(webUrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (segments.includes('shape')) {
|
|
||||||
throw new Error('Shape is currently not supported using Immoscout');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { query: rawParams } = queryString.parseUrl(webUrl, { arrayFormat: 'comma' });
|
const { query: rawParams } = queryString.parseUrl(webUrl, { arrayFormat: 'comma' });
|
||||||
const webParams = Object.fromEntries(
|
const webParams = Object.fromEntries(
|
||||||
Object.entries(rawParams).filter(([key]) => key !== 'enteredFrom' && PARAM_NAME_MAP[key]),
|
Object.entries(rawParams).filter(([key]) => key !== 'enteredFrom' && PARAM_NAME_MAP[key]),
|
||||||
@@ -179,18 +175,31 @@ export function convertWebToMobile(webUrl) {
|
|||||||
|
|
||||||
const geocodes = `/${segments.slice(2, segments.length - 1).join('/')}`;
|
const geocodes = `/${segments.slice(2, segments.length - 1).join('/')}`;
|
||||||
const isRadius = segments.includes('radius');
|
const isRadius = segments.includes('radius');
|
||||||
|
const isShape = segments.includes('shape');
|
||||||
const mobileParams = {
|
const mobileParams = {
|
||||||
searchType: isRadius ? 'radius' : 'region',
|
searchType: isRadius ? 'radius' : isShape ? 'shape' : 'region',
|
||||||
realestatetype: realType,
|
realestatetype: realType,
|
||||||
...(isRadius ? {} : { geocodes }),
|
...(isRadius || isShape ? {} : { geocodes }),
|
||||||
...additionalParamsFromWebPath,
|
...additionalParamsFromWebPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isShape && !webParams.shape) {
|
||||||
|
throw new Error('Shape search URL is missing the required "shape" query parameter');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isShape && webParams.shape) {
|
||||||
|
const browserShape = webParams.shape;
|
||||||
|
const normalized = browserShape.replace(/\.\./g, '==').replace(/\./g, '=');
|
||||||
|
const polyline = Buffer.from(normalized, 'base64').toString('utf-8');
|
||||||
|
mobileParams.shape = polyline;
|
||||||
|
}
|
||||||
|
|
||||||
if (webParams.geocoordinates) {
|
if (webParams.geocoordinates) {
|
||||||
mobileParams.geocoordinates = webParams.geocoordinates;
|
mobileParams.geocoordinates = webParams.geocoordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, val] of Object.entries(webParams)) {
|
for (const [key, val] of Object.entries(webParams)) {
|
||||||
|
if (key === 'shape') continue;
|
||||||
if (key === 'equipment') {
|
if (key === 'equipment') {
|
||||||
const items = [].concat(val).flatMap((v) => `${v}`.split(','));
|
const items = [].concat(val).flatMap((v) => `${v}`.split(','));
|
||||||
const currentEquipmentParams = mobileParams[PARAM_NAME_MAP[key]];
|
const currentEquipmentParams = mobileParams[PARAM_NAME_MAP[key]];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fredy",
|
"name": "fredy",
|
||||||
"version": "21.2.0",
|
"version": "21.3.1",
|
||||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ import { readFile } from 'fs/promises';
|
|||||||
export const testData = JSON.parse(await readFile(new URL('./testdata.json', import.meta.url)));
|
export const testData = JSON.parse(await readFile(new URL('./testdata.json', import.meta.url)));
|
||||||
|
|
||||||
describe('#immoscout-mobile URL conversion', () => {
|
describe('#immoscout-mobile URL conversion', () => {
|
||||||
|
// Test shape URL conversion
|
||||||
|
it('should convert a full web URL with shape to mobile URL', () => {
|
||||||
|
const webUrl =
|
||||||
|
'https://www.immobilienscout24.de/Suche/shape/haus-kaufen?shape=aW9yfkhfa3htQXJgUGlnYEBmekhte3BAcXNAfWBsQGNyQ2lkUHVvbEB3eX5Ab25WYn5Fa2BLaGRQY29FaGtTfEhme3xBdHBEdHFMamlHbmdRfHhMcmxPeHlWYnpS&price=-600000.0&ground=240.0-&enteredFrom=result_list';
|
||||||
|
const expectedMobileUrl =
|
||||||
|
'https://api.mobile.immobilienscout24.de/search/list?ground=240.0-&price=-600000.0&realestatetype=housebuy&searchType=shape&shape=ior~H_kxmAr%60Pig%60%40fzHm%7Bp%40qs%40%7D%60l%40crCidPuol%40wy~%40onVb~Ek%60KhdPcoEhkS%7CHf%7B%7CAtpDtqLjiGngQ%7CxLrlOxyVbzR';
|
||||||
|
|
||||||
|
const actualMobileUrl = convertWebToMobile(webUrl);
|
||||||
|
expect(actualMobileUrl).toBe(expectedMobileUrl);
|
||||||
|
});
|
||||||
|
|
||||||
// Test URL conversion
|
// Test URL conversion
|
||||||
it('should convert a full web URL to mobile URL', () => {
|
it('should convert a full web URL to mobile URL', () => {
|
||||||
const webUrl =
|
const webUrl =
|
||||||
|
|||||||
@@ -18,5 +18,9 @@
|
|||||||
"rentHouse": {
|
"rentHouse": {
|
||||||
"url": "https://www.immobilienscout24.de/Suche/de/nordrhein-westfalen/duesseldorf/haus-mieten?enteredFrom=one_step_search",
|
"url": "https://www.immobilienscout24.de/Suche/de/nordrhein-westfalen/duesseldorf/haus-mieten?enteredFrom=one_step_search",
|
||||||
"type": "houserent"
|
"type": "houserent"
|
||||||
|
},
|
||||||
|
"buyHouseWithShape": {
|
||||||
|
"url": "https://www.immobilienscout24.de/Suche/shape/haus-kaufen?shape=aW9yfkhfa3htQXJgUGlnYEBmekhte3BAcXNAfWBsQGNyQ2lkUHVvbEB3eX5Ab25WYn5Fa2BLaGRQY29FaGtTfEhme3xBdHBEdHFMamlHbmdRfHhMcmxPeHlWYnpS&price=-600000.0&ground=240.0-&enteredFrom=result_list",
|
||||||
|
"type": "housebuy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
ui/src/assets/news/2.png
Normal file
BIN
ui/src/assets/news/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 367 KiB |
@@ -1,11 +1,16 @@
|
|||||||
{
|
{
|
||||||
"key": "00e6b81777a275f5a140fc9101cb943810db6a69f6eb3927319c5aee0c876515",
|
"key": "00e6b81777a275f5a140fc9101cb943810db6a69f6eb3927319c5aee0c876221",
|
||||||
"content":
|
"content":
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"title": "Table overview for listings",
|
"title": "Table overview for listings",
|
||||||
"text": "Thanks to https://github.com/datenwurm, we now have a table overview for listings. If you decide to use the table view, the decision will be stored.",
|
"text": "Thanks to https://github.com/datenwurm, we now have a table overview for listings. If you decide to use the table view, the decision will be stored.",
|
||||||
"media": "1.png"
|
"media": "1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Table overview for jobs",
|
||||||
|
"text": "Based on datenwurm's, work, I created a table overview for jobs. If you decide to use the table view, the decision will be stored.",
|
||||||
|
"media": "2.png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Empty,
|
Empty,
|
||||||
Radio,
|
Radio,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
|
Tooltip,
|
||||||
} from '@douyinfe/semi-ui-19';
|
} from '@douyinfe/semi-ui-19';
|
||||||
import {
|
import {
|
||||||
IconAlertTriangle,
|
IconAlertTriangle,
|
||||||
@@ -35,6 +36,8 @@ import {
|
|||||||
IconArrowUp,
|
IconArrowUp,
|
||||||
IconArrowDown,
|
IconArrowDown,
|
||||||
IconHome,
|
IconHome,
|
||||||
|
IconGridView,
|
||||||
|
IconList,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import ListingDeletionModal from '../../ListingDeletionModal.jsx';
|
import ListingDeletionModal from '../../ListingDeletionModal.jsx';
|
||||||
@@ -42,6 +45,7 @@ import { useActions, useSelector } from '../../../services/state/store.js';
|
|||||||
import { xhrDelete, xhrPut, xhrPost } from '../../../services/xhr.js';
|
import { xhrDelete, xhrPut, xhrPost } from '../../../services/xhr.js';
|
||||||
import { debounce } from '../../../utils';
|
import { debounce } from '../../../utils';
|
||||||
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
|
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
|
||||||
|
import JobsTable from '../../table/JobsTable.jsx';
|
||||||
|
|
||||||
import './JobGrid.less';
|
import './JobGrid.less';
|
||||||
|
|
||||||
@@ -54,6 +58,9 @@ const JobGrid = () => {
|
|||||||
const actions = useActions();
|
const actions = useActions();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const userSettings = useSelector((state) => state.userSettings.settings);
|
||||||
|
const viewMode = userSettings?.jobs_view_mode ?? 'grid';
|
||||||
|
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const pageSize = 12;
|
const pageSize = 12;
|
||||||
|
|
||||||
@@ -234,6 +241,27 @@ const JobGrid = () => {
|
|||||||
onClick={() => setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))}
|
onClick={() => setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))}
|
||||||
title={sortDir === 'asc' ? 'Ascending' : 'Descending'}
|
title={sortDir === 'asc' ? 'Ascending' : 'Descending'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="jobGrid__topbar__view-toggle">
|
||||||
|
<Tooltip content="Grid view">
|
||||||
|
<Button
|
||||||
|
icon={<IconGridView />}
|
||||||
|
theme={viewMode === 'grid' ? 'solid' : 'borderless'}
|
||||||
|
onClick={() => actions.userSettings.setJobsViewMode('grid')}
|
||||||
|
aria-label="Grid view"
|
||||||
|
aria-pressed={viewMode === 'grid'}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Table view">
|
||||||
|
<Button
|
||||||
|
icon={<IconList />}
|
||||||
|
theme={viewMode === 'table' ? 'solid' : 'borderless'}
|
||||||
|
onClick={() => actions.userSettings.setJobsViewMode('table')}
|
||||||
|
aria-label="Table view"
|
||||||
|
aria-pressed={viewMode === 'table'}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(jobsData?.result || []).length === 0 && (
|
{(jobsData?.result || []).length === 0 && (
|
||||||
@@ -244,136 +272,144 @@ const JobGrid = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Row gutter={[16, 16]}>
|
{viewMode === 'grid' ? (
|
||||||
{(jobsData?.result || []).map((job) => (
|
<Row gutter={[16, 16]}>
|
||||||
<Col key={job.id} xs={24} sm={12} md={12} lg={8} xl={8} xxl={6}>
|
{(jobsData?.result || []).map((job) => (
|
||||||
<Card className="jobGrid__card" bodyStyle={{ padding: '16px' }}>
|
<Col key={job.id} xs={24} sm={12} md={12} lg={8} xl={8} xxl={6}>
|
||||||
<div className="jobGrid__card__header">
|
<Card className="jobGrid__card" bodyStyle={{ padding: '16px' }}>
|
||||||
<div className="jobGrid__card__name">
|
<div className="jobGrid__card__header">
|
||||||
<span className={`jobGrid__card__dot${job.enabled ? ' jobGrid__card__dot--active' : ''}`} />
|
<div className="jobGrid__card__name">
|
||||||
<Title heading={5} ellipsis={{ showTooltip: true }} className="jobGrid__title">
|
<span className={`jobGrid__card__dot${job.enabled ? ' jobGrid__card__dot--active' : ''}`} />
|
||||||
{job.name}
|
<Title heading={5} ellipsis={{ showTooltip: true }} className="jobGrid__title">
|
||||||
</Title>
|
{job.name}
|
||||||
|
</Title>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
|
||||||
|
{job.isOnlyShared && (
|
||||||
|
<Popover content={getPopoverContent('This job has been shared with you — read only.')}>
|
||||||
|
<div>
|
||||||
|
<IconAlertTriangle style={{ color: 'rgba(var(--semi-yellow-7), 1)' }} />
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
{job.running && (
|
||||||
|
<Tag color="green" variant="light" size="small">
|
||||||
|
RUNNING
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
|
|
||||||
{job.isOnlyShared && (
|
<div className="jobGrid__card__stats">
|
||||||
<Popover
|
<div className="jobGrid__card__stat jobGrid__card__stat--blue">
|
||||||
content={getPopoverContent(
|
<span className="jobGrid__card__stat__number">{job.numberOfFoundListings || 0}</span>
|
||||||
'This job has been shared with you by another user, therefor it is read-only.',
|
<span className="jobGrid__card__stat__label">
|
||||||
)}
|
<IconHome size="small" /> Listings
|
||||||
>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="jobGrid__card__stat jobGrid__card__stat--orange">
|
||||||
|
<span className="jobGrid__card__stat__number">{job.provider?.length || 0}</span>
|
||||||
|
<span className="jobGrid__card__stat__label">
|
||||||
|
<IconBriefcase size="small" /> Providers
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="jobGrid__card__stat jobGrid__card__stat--purple">
|
||||||
|
<span className="jobGrid__card__stat__number">{job.notificationAdapter?.length || 0}</span>
|
||||||
|
<span className="jobGrid__card__stat__label">
|
||||||
|
<IconBell size="small" /> Adapters
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider margin="12px" />
|
||||||
|
|
||||||
|
<div className="jobGrid__card__footer">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<Switch
|
||||||
|
onChange={(checked) => onJobStatusChanged(job.id, checked)}
|
||||||
|
checked={job.enabled}
|
||||||
|
disabled={job.isOnlyShared}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Text type="secondary" size="small">
|
||||||
|
Active
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="jobGrid__actions">
|
||||||
|
<Popover content={getPopoverContent('Run Job')}>
|
||||||
<div>
|
<div>
|
||||||
<IconAlertTriangle style={{ color: 'rgba(var(--semi-yellow-7), 1)' }} />
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ background: '#21aa21b5' }}
|
||||||
|
size="small"
|
||||||
|
theme="solid"
|
||||||
|
icon={<IconPlayCircle />}
|
||||||
|
disabled={job.isOnlyShared || job.running}
|
||||||
|
onClick={() => onJobRun(job.id)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
<Popover content={getPopoverContent('Edit a Job')}>
|
||||||
{job.running && (
|
<div>
|
||||||
<Tag color="green" variant="light" size="small">
|
<Button
|
||||||
RUNNING
|
type="secondary"
|
||||||
</Tag>
|
size="small"
|
||||||
)}
|
icon={<IconEdit />}
|
||||||
|
disabled={job.isOnlyShared}
|
||||||
|
onClick={() => navigate(`/jobs/edit/${job.id}`)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
<Popover content={getPopoverContent('Clone Job')}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="tertiary"
|
||||||
|
size="small"
|
||||||
|
icon={<IconCopy />}
|
||||||
|
disabled={job.isOnlyShared}
|
||||||
|
onClick={() => navigate('/jobs/new', { state: { cloneFrom: job.id } })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
<Popover content={getPopoverContent('Delete all found Listings of this Job')}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
icon={<IconDescend2 />}
|
||||||
|
disabled={job.isOnlyShared}
|
||||||
|
onClick={() => onListingRemoval(job.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
<Popover content={getPopoverContent('Delete Job')}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
icon={<IconDelete />}
|
||||||
|
disabled={job.isOnlyShared}
|
||||||
|
onClick={() => onJobRemoval(job.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
|
</Col>
|
||||||
<div className="jobGrid__card__stats">
|
))}
|
||||||
<div className="jobGrid__card__stat jobGrid__card__stat--blue">
|
</Row>
|
||||||
<span className="jobGrid__card__stat__number">{job.numberOfFoundListings || 0}</span>
|
) : (
|
||||||
<span className="jobGrid__card__stat__label">
|
<JobsTable
|
||||||
<IconHome size="small" /> Listings
|
jobs={jobsData?.result || []}
|
||||||
</span>
|
onRun={onJobRun}
|
||||||
</div>
|
onEdit={(id) => navigate(`/jobs/edit/${id}`)}
|
||||||
<div className="jobGrid__card__stat jobGrid__card__stat--orange">
|
onClone={(id) => navigate('/jobs/new', { state: { cloneFrom: id } })}
|
||||||
<span className="jobGrid__card__stat__number">{job.provider.length || 0}</span>
|
onDeleteListings={onListingRemoval}
|
||||||
<span className="jobGrid__card__stat__label">
|
onDeleteJob={onJobRemoval}
|
||||||
<IconBriefcase size="small" /> Providers
|
onStatusChange={onJobStatusChanged}
|
||||||
</span>
|
/>
|
||||||
</div>
|
)}
|
||||||
<div className="jobGrid__card__stat jobGrid__card__stat--purple">
|
|
||||||
<span className="jobGrid__card__stat__number">{job.notificationAdapter.length || 0}</span>
|
|
||||||
<span className="jobGrid__card__stat__label">
|
|
||||||
<IconBell size="small" /> Adapters
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider margin="12px" />
|
|
||||||
|
|
||||||
<div className="jobGrid__card__footer">
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
||||||
<Switch
|
|
||||||
onChange={(checked) => onJobStatusChanged(job.id, checked)}
|
|
||||||
checked={job.enabled}
|
|
||||||
disabled={job.isOnlyShared}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<Text type="secondary" size="small">
|
|
||||||
Active
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="jobGrid__actions">
|
|
||||||
<Popover content={getPopoverContent('Run Job')}>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
style={{ background: '#21aa21b5' }}
|
|
||||||
size="small"
|
|
||||||
theme="solid"
|
|
||||||
icon={<IconPlayCircle />}
|
|
||||||
disabled={job.isOnlyShared || job.running}
|
|
||||||
onClick={() => onJobRun(job.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
<Popover content={getPopoverContent('Edit a Job')}>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
type="secondary"
|
|
||||||
size="small"
|
|
||||||
icon={<IconEdit />}
|
|
||||||
disabled={job.isOnlyShared}
|
|
||||||
onClick={() => navigate(`/jobs/edit/${job.id}`)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
<Popover content={getPopoverContent('Clone Job')}>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
type="tertiary"
|
|
||||||
size="small"
|
|
||||||
icon={<IconCopy />}
|
|
||||||
disabled={job.isOnlyShared}
|
|
||||||
onClick={() => navigate('/jobs/new', { state: { cloneFrom: job.id } })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
<Popover content={getPopoverContent('Delete all found Listings of this Job')}>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
type="danger"
|
|
||||||
size="small"
|
|
||||||
icon={<IconDescend2 />}
|
|
||||||
disabled={job.isOnlyShared}
|
|
||||||
onClick={() => onListingRemoval(job.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
<Popover content={getPopoverContent('Delete Job')}>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
type="danger"
|
|
||||||
size="small"
|
|
||||||
icon={<IconDelete />}
|
|
||||||
disabled={job.isOnlyShared}
|
|
||||||
onClick={() => onJobRemoval(job.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
{(jobsData?.result || []).length > 0 && jobsData?.totalNumber > 12 && (
|
{(jobsData?.result || []).length > 0 && jobsData?.totalNumber > 12 && (
|
||||||
<div className="jobGrid__pagination">
|
<div className="jobGrid__pagination">
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -17,6 +17,12 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__view-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__card {
|
&__card {
|
||||||
|
|||||||
128
ui/src/components/table/JobsTable.jsx
Normal file
128
ui/src/components/table/JobsTable.jsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button, Tag, Tooltip, Switch } from '@douyinfe/semi-ui-19';
|
||||||
|
import {
|
||||||
|
IconAlertTriangle,
|
||||||
|
IconBell,
|
||||||
|
IconBriefcase,
|
||||||
|
IconCopy,
|
||||||
|
IconDelete,
|
||||||
|
IconDescend2,
|
||||||
|
IconEdit,
|
||||||
|
IconHome,
|
||||||
|
IconPlayCircle,
|
||||||
|
} from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import './JobsTable.less';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ jobs: object[], onRun: Function, onEdit: Function, onClone: Function, onDeleteListings: Function, onDeleteJob: Function, onStatusChange: Function }} props
|
||||||
|
*/
|
||||||
|
const JobsTable = ({ jobs, onRun, onEdit, onClone, onDeleteListings, onDeleteJob, onStatusChange }) => (
|
||||||
|
<div className="jobsTable">
|
||||||
|
{jobs.map((job) => (
|
||||||
|
<div key={job.id} className={`jobsTable__row${!job.enabled ? ' jobsTable__row--inactive' : ''}`}>
|
||||||
|
<div className="jobsTable__row__dot">
|
||||||
|
<span
|
||||||
|
className={`jobsTable__row__dot__indicator${job.enabled ? ' jobsTable__row__dot__indicator--active' : ''}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="jobsTable__row__name" title={job.name}>
|
||||||
|
{job.name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="jobsTable__row__stat jobsTable__row__stat--blue">
|
||||||
|
<IconHome size="small" />
|
||||||
|
{job.numberOfFoundListings || 0}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="jobsTable__row__stat jobsTable__row__stat--orange">
|
||||||
|
<IconBriefcase size="small" />
|
||||||
|
{job.provider?.length || 0}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="jobsTable__row__stat jobsTable__row__stat--purple">
|
||||||
|
<IconBell size="small" />
|
||||||
|
{job.notificationAdapter?.length || 0}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="jobsTable__row__badges">
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={job.enabled}
|
||||||
|
disabled={job.isOnlyShared}
|
||||||
|
onChange={(checked) => onStatusChange(job.id, checked)}
|
||||||
|
/>
|
||||||
|
{job.running && (
|
||||||
|
<Tag color="green" variant="light" size="small">
|
||||||
|
RUNNING
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{job.isOnlyShared && (
|
||||||
|
<Tooltip content="Shared with you — read only">
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<IconAlertTriangle style={{ color: 'rgba(var(--semi-yellow-7), 1)' }} />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="jobsTable__row__actions">
|
||||||
|
<Tooltip content="Run Job">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ background: '#21aa21b5' }}
|
||||||
|
size="small"
|
||||||
|
theme="solid"
|
||||||
|
icon={<IconPlayCircle />}
|
||||||
|
disabled={job.isOnlyShared || job.running}
|
||||||
|
onClick={() => onRun(job.id)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Edit Job">
|
||||||
|
<Button
|
||||||
|
type="secondary"
|
||||||
|
size="small"
|
||||||
|
icon={<IconEdit />}
|
||||||
|
disabled={job.isOnlyShared}
|
||||||
|
onClick={() => onEdit(job.id)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Clone Job">
|
||||||
|
<Button
|
||||||
|
type="tertiary"
|
||||||
|
size="small"
|
||||||
|
icon={<IconCopy />}
|
||||||
|
disabled={job.isOnlyShared}
|
||||||
|
onClick={() => onClone(job.id)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Delete all found Listings">
|
||||||
|
<Button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
icon={<IconDescend2 />}
|
||||||
|
disabled={job.isOnlyShared}
|
||||||
|
onClick={() => onDeleteListings(job.id)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Delete Job">
|
||||||
|
<Button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
icon={<IconDelete />}
|
||||||
|
disabled={job.isOnlyShared}
|
||||||
|
onClick={() => onDeleteJob(job.id)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default JobsTable;
|
||||||
105
ui/src/components/table/JobsTable.less
Normal file
105
ui/src/components/table/JobsTable.less
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
@import '../../tokens.less';
|
||||||
|
|
||||||
|
.jobsTable {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 24px 1fr 80px 80px 80px auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: @space-3;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: @color-elevated;
|
||||||
|
border: 1px solid @color-border;
|
||||||
|
border-radius: @radius-chip;
|
||||||
|
transition: background @transition-fast;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--inactive {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&__indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: rgba(251, 113, 133, 0.7);
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background-color: rgba(52, 211, 153, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: @text-sm;
|
||||||
|
color: @color-text;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat {
|
||||||
|
font-size: @text-sm;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
color: @color-blue-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--orange {
|
||||||
|
color: @color-orange-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--purple {
|
||||||
|
color: @color-purple-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__badges {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
grid-template-columns: 24px 1fr 80px auto auto;
|
||||||
|
|
||||||
|
.jobsTable__row__stat--orange,
|
||||||
|
.jobsTable__row__stat--purple {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
grid-template-columns: 24px 1fr auto auto;
|
||||||
|
|
||||||
|
.jobsTable__row__stat--blue {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -335,6 +335,20 @@ export const useFredyState = create(
|
|||||||
throw Exception;
|
throw Exception;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async setJobsViewMode(jobs_view_mode) {
|
||||||
|
try {
|
||||||
|
await xhrPost('/api/user/settings/jobs-view-mode', { jobs_view_mode });
|
||||||
|
set((state) => ({
|
||||||
|
userSettings: {
|
||||||
|
...state.userSettings,
|
||||||
|
settings: { ...state.userSettings.settings, jobs_view_mode },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} catch (Exception) {
|
||||||
|
console.error('Error while trying to update jobs view mode setting. Error:', Exception);
|
||||||
|
throw Exception;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -141,21 +141,6 @@ export default function ProviderMutator({
|
|||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Banner
|
|
||||||
fullMode={false}
|
|
||||||
type="warning"
|
|
||||||
closeIcon={null}
|
|
||||||
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Warning</div>}
|
|
||||||
style={{ marginBottom: '1rem' }}
|
|
||||||
description={
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
Currently, our Immoscout implementation does not support drawing shapes on a map. Use a radius instead.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
filter
|
filter
|
||||||
placeholder="Select a provider"
|
placeholder="Select a provider"
|
||||||
|
|||||||
Reference in New Issue
Block a user