2025-12-11 10:40:55 +01:00
/ *
* Copyright ( c ) 2025 by Christian Kellner .
* Licensed under Apache - 2.0 with Commons Clause and Attribution / Naming Clause
* /
2021-01-21 16:09:23 +01:00
import React , { Fragment , useState } from 'react' ;
import NotificationAdapterMutator from './components/notificationAdapter/NotificationAdapterMutator' ;
import NotificationAdapterTable from '../../../components/table/NotificationAdapterTable' ;
import ProviderTable from '../../../components/table/ProviderTable' ;
import ProviderMutator from './components/provider/ProviderMutator' ;
import Headline from '../../../components/headline/Headline' ;
2025-09-18 20:09:11 +02:00
import { useActions , useSelector } from '../../../services/state/store' ;
2021-01-21 16:09:23 +01:00
import { xhrPost } from '../../../services/xhr' ;
2025-09-02 20:18:37 +02:00
import { useNavigate , useParams } from 'react-router-dom' ;
2025-10-07 21:06:59 +02:00
import { Divider , Input , Switch , Button , TagInput , Toast , Select } from '@douyinfe/semi-ui' ;
2021-01-21 16:09:23 +01:00
import './JobMutation.less' ;
2022-03-27 19:42:58 +02:00
import { SegmentPart } from '../../../components/segment/SegmentPart' ;
2025-11-18 12:24:27 +01:00
import {
IconBell ,
IconBriefcase ,
IconPaperclip ,
IconPlayCircle ,
IconPlusCircle ,
IconUser ,
IconClear ,
} from '@douyinfe/semi-icons' ;
2021-01-21 16:09:23 +01:00
export default function JobMutator ( ) {
2025-12-23 08:47:51 +01:00
const jobs = useSelector ( ( state ) => state . jobsData . jobs ) ;
const shareableUserList = useSelector ( ( state ) => state . jobsData . shareableUserList ) ;
2021-01-21 16:09:23 +01:00
const params = useParams ( ) ;
const jobToBeEdit = params . jobId == null ? null : jobs . find ( ( job ) => job . id === params . jobId ) ;
const defaultBlacklist = jobToBeEdit ? . blacklist || [ ] ;
const defaultName = jobToBeEdit ? . name || null ;
const defaultProviderData = jobToBeEdit ? . provider || [ ] ;
const defaultNotificationAdapter = jobToBeEdit ? . notificationAdapter || [ ] ;
const defaultEnabled = jobToBeEdit ? . enabled ? ? true ;
2025-11-26 17:10:42 +01:00
const [ providerToEdit , setProviderToEdit ] = useState ( null ) ;
2021-01-21 16:09:23 +01:00
const [ providerCreationVisible , setProviderCreationVisibility ] = useState ( false ) ;
const [ notificationCreationVisible , setNotificationCreationVisibility ] = useState ( false ) ;
2021-05-20 14:21:29 +02:00
const [ editNotificationAdapter , setEditNotificationAdapter ] = useState ( null ) ;
2021-01-21 16:09:23 +01:00
const [ providerData , setProviderData ] = useState ( defaultProviderData ) ;
const [ name , setName ] = useState ( defaultName ) ;
const [ blacklist , setBlacklist ] = useState ( defaultBlacklist ) ;
const [ notificationAdapterData , setNotificationAdapterData ] = useState ( defaultNotificationAdapter ) ;
2025-10-07 21:06:59 +02:00
const [ shareWithUsers , setShareWithUsers ] = useState ( jobToBeEdit ? . shared _with _user ? ? [ ] ) ;
2021-01-21 16:09:23 +01:00
const [ enabled , setEnabled ] = useState ( defaultEnabled ) ;
2025-09-02 20:18:37 +02:00
const navigate = useNavigate ( ) ;
2025-09-18 20:09:11 +02:00
const actions = useActions ( ) ;
2021-01-21 16:09:23 +01:00
const isSavingEnabled = ( ) => {
2025-08-01 09:51:42 +02:00
return Boolean ( notificationAdapterData . length && providerData . length && name ) ;
2021-01-21 16:09:23 +01:00
} ;
2025-11-26 17:10:42 +01:00
const handleProviderEdit = ( data ) => {
setProviderData (
providerData . map ( ( provider ) => ( provider . url === data . oldProviderToEdit . url ? data . newData : provider ) ) ,
) ;
} ;
2021-01-21 16:09:23 +01:00
const mutateJob = async ( ) => {
try {
await xhrPost ( '/api/jobs' , {
provider : providerData ,
notificationAdapter : notificationAdapterData ,
2025-10-07 21:06:59 +02:00
shareWithUsers ,
2021-01-21 16:09:23 +01:00
name ,
blacklist ,
enabled ,
jobId : jobToBeEdit ? . id || null ,
} ) ;
2025-12-23 08:47:51 +01:00
await actions . jobsData . getJobs ( ) ;
2023-03-20 08:52:13 +01:00
Toast . success ( 'Job successfully saved...' ) ;
2025-09-02 20:18:37 +02:00
navigate ( '/jobs' ) ;
2021-01-21 16:09:23 +01:00
} catch ( Exception ) {
2021-05-13 17:22:59 +02:00
console . error ( Exception . json . message ) ;
2023-03-20 08:52:13 +01:00
Toast . error ( Exception . json != null ? Exception . json . message : Exception ) ;
2021-01-21 16:09:23 +01:00
}
} ;
return (
< Fragment >
< ProviderMutator
visible = { providerCreationVisible }
onVisibilityChanged = { ( visible ) => setProviderCreationVisibility ( visible ) }
onData = { ( data ) => {
setProviderData ( [ ... providerData , data ] ) ;
} }
2025-11-26 17:10:42 +01:00
onEditData = { handleProviderEdit }
providerToEdit = { providerToEdit }
2021-01-21 16:09:23 +01:00
/ >
2021-05-20 14:21:29 +02:00
{ notificationCreationVisible && (
< NotificationAdapterMutator
visible = { notificationCreationVisible }
onVisibilityChanged = { ( visible ) => {
setEditNotificationAdapter ( null ) ;
setNotificationCreationVisibility ( visible ) ;
} }
selected = { notificationAdapterData }
editNotificationAdapter = {
editNotificationAdapter == null
? null
: notificationAdapterData . find ( ( adapter ) => adapter . id === editNotificationAdapter )
}
onData = { ( data ) => {
const oldData = [ ... notificationAdapterData ] . filter ( ( o ) => o . id !== data . id ) ;
setNotificationAdapterData ( [ ... oldData , data ] ) ;
} }
/ >
) }
2021-01-21 16:09:23 +01:00
2025-09-27 18:01:42 +02:00
< Headline text = { jobToBeEdit ? 'Edit Job' : 'Create new Job' } / >
2023-03-20 08:52:13 +01:00
< form >
2025-10-07 21:06:59 +02:00
< SegmentPart name = "Name" Icon = { IconPaperclip } >
2023-03-20 08:52:13 +01:00
< Input
2025-05-27 12:01:26 +02:00
autoFocus
2021-01-21 16:09:23 +01:00
type = "text"
maxLength = { 40 }
placeholder = "Name"
width = { 6 }
2023-03-20 08:52:13 +01:00
value = { name }
onChange = { ( value ) => setName ( value ) }
2021-01-21 16:09:23 +01:00
/ >
2022-03-27 19:42:58 +02:00
< / SegmentPart >
2023-03-20 08:52:13 +01:00
< Divider margin = "1rem" / >
2022-03-27 19:42:58 +02:00
< SegmentPart
2025-08-01 09:51:42 +02:00
name = "Providers"
2025-10-07 21:06:59 +02:00
Icon = { IconBriefcase }
2025-08-01 09:51:42 +02:00
helpText = { `
A provider is essentially the service ( e . g . ImmoScout24 , Kleinanzeigen ) that Fredy searches for new listings .
Fredy will open a new tab pointing to the website of this provider . You have to adjust your search parameter
and click on "Search" . If the results are being shown , copy the browser URL in here .
` }
2022-03-27 19:42:58 +02:00
>
2023-03-20 08:52:13 +01:00
< Button
type = "primary"
icon = { < IconPlusCircle / > }
className = "jobMutation__newButton"
2025-11-26 17:10:42 +01:00
onClick = { ( ) => {
setProviderToEdit ( null ) ;
setProviderCreationVisibility ( true ) ;
} }
2023-03-20 08:52:13 +01:00
>
2022-03-27 19:42:58 +02:00
Add new Provider
2023-03-20 08:52:13 +01:00
< / Button >
2021-01-21 16:09:23 +01:00
< ProviderTable
providerData = { providerData }
2025-07-19 20:10:19 +02:00
onRemove = { ( providerUrl ) => {
setProviderData ( providerData . filter ( ( provider ) => provider . url !== providerUrl ) ) ;
2021-01-21 16:09:23 +01:00
} }
2025-11-26 17:10:42 +01:00
onEdit = { ( provider ) => {
setProviderCreationVisibility ( true ) ;
setProviderToEdit ( provider ) ;
} }
2021-01-21 16:09:23 +01:00
/ >
2022-03-27 19:42:58 +02:00
< / SegmentPart >
2023-03-20 08:52:13 +01:00
< Divider margin = "1rem" / >
2022-03-27 19:42:58 +02:00
< SegmentPart
2025-10-07 21:06:59 +02:00
Icon = { IconBell }
2025-08-01 09:51:42 +02:00
name = "Notification Adapters"
2022-03-27 19:42:58 +02:00
helpText = "Fredy supports multiple ways to notify you about new findings. These are called notification adapter. You can chose between email, Telegram etc."
>
2023-03-20 08:52:13 +01:00
< Button
type = "primary"
2022-03-27 19:42:58 +02:00
className = "jobMutation__newButton"
2023-03-20 08:52:13 +01:00
icon = { < IconPlusCircle / > }
2022-03-27 19:42:58 +02:00
onClick = { ( ) => setNotificationCreationVisibility ( true ) }
>
Add new Notification Adapter
2023-03-20 08:52:13 +01:00
< / Button >
2021-01-21 16:09:23 +01:00
< NotificationAdapterTable
notificationAdapter = { notificationAdapterData }
onRemove = { ( adapterId ) => {
2021-05-20 14:21:29 +02:00
setEditNotificationAdapter ( null ) ;
2021-01-21 16:09:23 +01:00
setNotificationAdapterData ( notificationAdapterData . filter ( ( adapter ) => adapter . id !== adapterId ) ) ;
} }
2021-05-20 14:21:29 +02:00
onEdit = { ( adapterId ) => {
setEditNotificationAdapter ( adapterId ) ;
setNotificationCreationVisibility ( true ) ;
} }
2021-01-21 16:09:23 +01:00
/ >
2022-03-27 19:42:58 +02:00
< / SegmentPart >
2023-03-20 08:52:13 +01:00
< Divider margin = "1rem" / >
2022-03-27 19:42:58 +02:00
< SegmentPart
2025-11-18 12:24:27 +01:00
Icon = { IconClear }
2022-03-27 19:42:58 +02:00
name = "Blacklist"
2023-03-20 08:52:13 +01:00
helpText = "If a listing contains one of these words, it will be filtered out. Type in a word, then hit enter."
2022-03-27 19:42:58 +02:00
>
2023-03-20 08:52:13 +01:00
< TagInput
value = { blacklist || [ ] }
placeholder = "Add a word for filtering..."
onChange = { ( v ) => setBlacklist ( [ ... v ] ) }
2021-01-21 16:09:23 +01:00
/ >
2022-03-27 19:42:58 +02:00
< / SegmentPart >
2023-03-20 08:52:13 +01:00
< Divider margin = "1rem" / >
2022-03-27 19:42:58 +02:00
< SegmentPart
2025-10-07 21:06:59 +02:00
Icon = { IconUser }
name = "Sharing with user"
helpText = "You can share this job with other users. They will be able to see the listings, but only (as the creator) you can edit the job. Admins are filtered from this list as they have access to everything."
>
{ shareableUserList . length === 0 ? (
< div > No users found to share this Job to . Please create additional non - admin user . < / div >
) : (
< Select
filter
multiple
placeholder = "Search user"
autoClearSearchValue = { false }
defaultValue = { shareWithUsers }
onChange = { ( value ) => setShareWithUsers ( value ) }
>
{ shareableUserList . map ( ( user ) => (
< Select.Option value = { user . id } key = { user . id } >
{ user . name }
< / Select.Option >
) ) }
< / Select >
) }
< / SegmentPart >
< Divider margin = "1rem" / >
< SegmentPart
Icon = { IconPlayCircle }
2022-03-27 19:42:58 +02:00
name = "Job activation"
2025-08-01 09:51:42 +02:00
helpText = "Whether or not the job is activated. Inactive jobs will be ignored when Fredy checks for new listings."
2022-03-27 19:42:58 +02:00
>
2021-01-21 16:09:23 +01:00
< Switch className = "jobMutation__spaceTop" onChange = { ( checked ) => setEnabled ( checked ) } checked = { enabled } / >
2022-03-27 19:42:58 +02:00
< / SegmentPart >
2023-03-20 08:52:13 +01:00
< Divider margin = "1rem" / >
2025-09-02 20:18:37 +02:00
< Button type = "danger" style = { { marginRight : '1rem' } } onClick = { ( ) => navigate ( '/jobs' ) } >
2021-01-21 16:09:23 +01:00
Cancel
< / Button >
2023-03-20 08:52:13 +01:00
< Button type = "primary" icon = { < IconPlusCircle / > } disabled = { ! isSavingEnabled ( ) } onClick = { mutateJob } >
2021-01-21 16:09:23 +01:00
Save
< / Button >
2023-03-20 08:52:13 +01:00
< / form >
2021-01-21 16:09:23 +01:00
< / Fragment >
) ;
}