Compare commits

...

3 Commits

Author SHA1 Message Date
orangecoding
2f00966f27 next release version 2026-05-07 19:12:17 +02:00
orangecoding
921057252d adding immoscout shape search 2026-05-07 19:11:47 +02:00
orangecoding
703c602527 table overview for jobs 2026-05-07 15:59:55 +02:00
13 changed files with 468 additions and 148 deletions

View File

@@ -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 });
}
});
} }

View File

@@ -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]];

View File

@@ -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",

View File

@@ -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 =

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

View File

@@ -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"
} }
] ]
} }

View File

@@ -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

View File

@@ -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 {

View 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;

View 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;
}
}
}
}

View File

@@ -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;
}
},
}, },
}; };

View File

@@ -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"