From e25d1e6a9da11cc7456ea170d2d015af82b18718 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Fri, 6 Mar 2026 04:59:03 +0530 Subject: [PATCH 1/5] add jobs schedule & edit options --- .../Data/Jobs/EditScheduledJobModal.react.js | 373 ++++++++++++++++++ src/dashboard/Data/Jobs/Jobs.react.js | 275 +++++++++---- src/dashboard/Data/Jobs/Jobs.scss | 28 ++ src/lib/ParseApp.js | 20 + src/lib/stores/JobsStore.js | 43 +- 5 files changed, 659 insertions(+), 80 deletions(-) create mode 100644 src/dashboard/Data/Jobs/EditScheduledJobModal.react.js diff --git a/src/dashboard/Data/Jobs/EditScheduledJobModal.react.js b/src/dashboard/Data/Jobs/EditScheduledJobModal.react.js new file mode 100644 index 0000000000..278cc4f8c0 --- /dev/null +++ b/src/dashboard/Data/Jobs/EditScheduledJobModal.react.js @@ -0,0 +1,373 @@ +import React, { useState, useEffect } from 'react'; +import B4aModal from 'components/B4aModal/B4aModal.react'; +import B4aToggle from 'components/Toggle/B4aToggle.react'; +import DateTimeInput from 'components/DateTimeInput/DateTimeInput.react'; +import Dropdown from 'components/Dropdown/Dropdown.react'; +import Field from 'components/Field/Field.react'; +import FormNote from 'components/FormNote/FormNote.react'; +import IntervalInput from 'components/IntervalInput/IntervalInput.react'; +import Label from 'components/Label/Label.react'; +import Option from 'components/Dropdown/Option.react'; +import TextInput from 'components/TextInput/TextInput.react'; +import TimeInput from 'components/TimeInput/TimeInput.react'; +import { hoursFrom } from 'lib/DateUtils'; + +function parseInitialState(job) { + if (!job) { + return { + name: '', + cloudCodeFunction: '', + parameter: '', + startAt: hoursFrom(new Date(), 1), + repeat: false, + repeatType: 'Every day', + intervalCount: 15, + intervalUnit: 'minute', + repeatHour: '12', + repeatMinute: '00', + }; + } + + const startAfter = job.startAfter ? new Date(job.startAfter) : hoursFrom(new Date(), 1); + let repeat = false; + let repeatType = 'Every day'; + let intervalCount = 15; + let intervalUnit = 'minute'; + let repeatHour = '12'; + let repeatMinute = '00'; + + if (job.repeatMinutes) { + repeat = true; + if (job.repeatMinutes === 1440) { + repeatType = 'Every day'; + } else { + repeatType = 'On an interval'; + if (job.repeatMinutes > 60) { + intervalCount = (job.repeatMinutes / 60) | 0; + intervalUnit = 'hour'; + } else { + intervalCount = job.repeatMinutes; + intervalUnit = 'minute'; + } + } + } + + if (job.timeOfDay) { + const parts = job.timeOfDay.split(':'); + repeatHour = parts[0] ? parts[0].replace(/^0/, '') || '0' : '12'; + repeatMinute = parts[1] || '00'; + } + + return { + name: job.description || '', + cloudCodeFunction: job.jobName || '', + parameter: job.params || '', + startAt: startAfter, + repeat, + repeatType, + intervalCount, + intervalUnit, + repeatHour, + repeatMinute, + }; +} + +const FIELD_PADDING = { padding: '0 1rem', width: '100%' }; + +const EditScheduledJobModal = ({ job, context, onCancel, onSuccess }) => { + const isCreate = !job; + const initial = parseInitialState(job); + + const [name, setName] = useState(initial.name); + const [cloudCodeFunction, setCloudCodeFunction] = useState(initial.cloudCodeFunction); + const [parameter, setParameter] = useState(initial.parameter); + const [startAt, setStartAt] = useState(initial.startAt); + const [repeat, setRepeat] = useState(initial.repeat); + const [repeatType, setRepeatType] = useState(initial.repeatType); + const [intervalCount, setIntervalCount] = useState(initial.intervalCount); + const [intervalUnit, setIntervalUnit] = useState(initial.intervalUnit); + const [repeatHour, setRepeatHour] = useState(initial.repeatHour); + const [repeatMinute, setRepeatMinute] = useState(initial.repeatMinute); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [paramError, setParamError] = useState(null); + const [availableJobs, setAvailableJobs] = useState([]); + const [loadingJobs, setLoadingJobs] = useState(isCreate); + + useEffect(() => { + if (!isCreate) { + return; + } + context.getAvailableJobs() + .then(result => { + const jobs = (result && result.jobs) || []; + setAvailableJobs(jobs); + if (jobs.length > 0) { + setCloudCodeFunction(jobs[0]); + } + }) + .catch(() => setAvailableJobs([])) + .finally(() => setLoadingJobs(false)); + }, []); + + function buildPayload() { + let parsedParam = null; + if (parameter && parameter.trim() !== '') { + parsedParam = JSON.parse(parameter); + } + + let dailyRun = null; + let intervalRun = null; + + if (repeat) { + const hour = repeatHour.length < 2 ? '0' + repeatHour : repeatHour; + if (repeatType === 'Every day') { + dailyRun = `${hour}:${repeatMinute}`; + } else { + intervalRun = intervalCount * (intervalUnit === 'hour' ? 60 : 1); + dailyRun = `${hour}:${repeatMinute}`; + } + } + + return { + job: { + name, + cloudCodeFunction, + parameter: parsedParam, + schedule: { + start: startAt.toISOString(), + dailyRun, + intervalRun, + }, + }, + }; + } + + function validateParameter(value) { + if (!value || value.trim() === '') { + return null; + } + let parsed; + try { + parsed = JSON.parse(value); + } catch (e) { + return 'Parameters must be valid JSON.'; + } + if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) { + return 'Parameters must be a JSON object, e.g. { "key": "value" }.'; + } + return null; + } + + function validate() { + if (!name.trim()) { + return 'A name is required.'; + } + if (!cloudCodeFunction) { + return 'A cloud code function is required.'; + } + const paramErr = validateParameter(parameter); + if (paramErr) { + return paramErr; + } + return null; + } + + function handleConfirm() { + const validationError = validate(); + if (validationError) { + setError(validationError); + return; + } + setSaving(true); + setError(null); + const payload = buildPayload(); + const request = isCreate + ? context.createScheduledJob(payload) + : context.updateScheduledJob(job.objectId, payload); + request + .then(() => { + setSaving(false); + onSuccess(); + }) + .catch(err => { + setSaving(false); + setError((err && (err.error || err.message)) || `Failed to ${isCreate ? 'create' : 'update'} job. Please try again.`); + }); + } + + return ( + +
+ + + } + input={ + { setName(val); setError(null); }} + /> + } + /> + + } + input={ + isCreate ? ( + loadingJobs ? ( + + ) : ( + { setCloudCodeFunction(val); setError(null); }} + > + {availableJobs.map(jobName => ( + + ))} + + ) + ) : ( + + ) + } + /> + + + } + input={ +
+ { + setParameter(val); + setParamError(validateParameter(val)); + setError(null); + }} + /> + {paramError && ( +
+ {paramError} +
+ )} +
+ } + /> + + } + input={ +
+ +
+ } + /> + + + } + input={ +
+ +
+ } + /> + + {repeat && ( + } + input={ +
+ +
+ } + /> + )} + + {repeat && repeatType === 'On an interval' && ( + } + input={ +
+ { setIntervalCount(count); setIntervalUnit(unit); }} + /> +
+ } + /> + )} + + {repeat && ( + + } + input={ +
+ { setRepeatHour(h); setRepeatMinute(m); }} + /> +
+ } + /> + )} + + {error && ( + {error} + )} +
+
+ ); +}; + +export default EditScheduledJobModal; diff --git a/src/dashboard/Data/Jobs/Jobs.react.js b/src/dashboard/Data/Jobs/Jobs.react.js index 3abdd59a0d..e395d8b90a 100644 --- a/src/dashboard/Data/Jobs/Jobs.react.js +++ b/src/dashboard/Data/Jobs/Jobs.react.js @@ -12,8 +12,9 @@ import CategoryList from 'components/CategoryList/CategoryList.react'; import EmptyGhostState from 'components/EmptyGhostState/EmptyGhostState.react'; import ChromeDropdown from 'components/ChromeDropdown/ChromeDropdown.react'; import Icon from 'components/Icon/Icon.react'; -import JobScheduleReminder from 'dashboard/Data/Jobs/JobScheduleReminder.react'; -import Modal from 'components/Modal/Modal.react'; +import B4aModal from 'components/B4aModal/B4aModal.react'; +import EditScheduledJobModal from 'dashboard/Data/Jobs/EditScheduledJobModal.react'; +import FormNote from 'components/FormNote/FormNote.react'; import Popover from 'components/Popover/Popover.react'; import Position from 'lib/Position'; import React from 'react'; @@ -34,7 +35,8 @@ import { withRouter } from 'lib/withRouter'; const subsections = { all: 'All Jobs', - // scheduled: 'Scheduled Jobs', + scheduled: 'Scheduled Jobs', + 'scheduled-jobs': 'Scheduled Jobs', status: 'Job Status', }; @@ -44,7 +46,22 @@ const statusColors = { running: 'blue', }; +const MONTH_ABBREVS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + +function formatScheduleDate(date) { + const month = MONTH_ABBREVS[date.getUTCMonth()]; + const day = date.getUTCDate(); + const hours = (date.getUTCHours() < 10 ? '0' : '') + date.getUTCHours(); + const minutes = (date.getUTCMinutes() < 10 ? '0' : '') + date.getUTCMinutes(); + return `${month} ${day} at ${hours}:${minutes}`; +} + function scheduleString(data) { + const runAt = new Date(data.startAfter); + if (Number.isNaN(runAt.getTime())) { + return
-
; + } + let schedule = ''; if (data.repeatMinutes) { if (data.repeatMinutes === 1440) { @@ -54,22 +71,12 @@ function scheduleString(data) { } else { schedule += 'Each day, every ' + data.repeatMinutes + ' minutes, '; } - schedule += 'after ' + data.timeOfDay.substr(0, 5) + ', '; + if (data.timeOfDay) { + schedule += 'after ' + data.timeOfDay.substr(0, 5) + ', '; + } schedule += 'starting '; - } else { - schedule = 'On '; } - const runAt = new Date(data.startAfter); - schedule += - runAt.getUTCMonth() + '/' + runAt.getUTCDate() + '/' + String(runAt.getUTCFullYear()).substr(2); - schedule += - ' at ' + - (runAt.getUTCHours() < 10 ? '0' : '') + - runAt.getUTCHours() + - ':' + - (runAt.getUTCMinutes() < 10 ? '0' : '') + - runAt.getUTCMinutes() + - '.'; + schedule += formatScheduleDate(runAt); return
{schedule}
; } @@ -84,6 +91,10 @@ class Jobs extends TableView { this.state = { toDelete: null, + deleteInProgress: false, + deleteError: null, + toEdit: null, + toCreate: false, jobStatus: undefined, loading: true, // Properties used to control data access @@ -111,19 +122,28 @@ class Jobs extends TableView { if (nextProps.availableJobs) { if (nextProps.availableJobs.length > 0) { this.action = new SidebarAction(Schedule job, this.navigateToNew.bind(this)); - return; } + } else { + this.action = null; } - // check if the changes are in currentApp serverInfo status - // if not return without making any request + + const sectionChanged = nextProps.params.section !== this.props.params.section; + const appChanged = nextProps.params.appId !== this.props.params.appId; + let appStatusChanged = false; + if (this.props.apps !== nextProps.apps) { - const updatedCurrentApp = nextProps.apps.find(ap => ap.slug === this.props.params.appId); + const updatedCurrentApp = nextProps.apps.find(ap => ap.slug === nextProps.params.appId); const prevCurrentApp = this.props.apps.find(ap => ap.slug === this.props.params.appId); - const shouldUpdate = updatedCurrentApp.serverInfo.status !== prevCurrentApp.serverInfo.status; - if (!shouldUpdate) {return;} + const updatedStatus = updatedCurrentApp && updatedCurrentApp.serverInfo && updatedCurrentApp.serverInfo.status; + const prevStatus = prevCurrentApp && prevCurrentApp.serverInfo && prevCurrentApp.serverInfo.status; + appStatusChanged = updatedStatus !== prevStatus; } - this.action = null; - this.loadData(); + + if (!sectionChanged && !appChanged && !appStatusChanged) { + return; + } + + this.loadData(nextProps.params.section); } navigateToNew() { @@ -134,8 +154,24 @@ class Jobs extends TableView { this.props.navigate(generatePath(this.context, `jobs/edit/${jobId}`)); } - loadData() { - this.props.jobs.dispatch(ActionTypes.FETCH).finally(() => { + getCurrentSection() { + return this.props.params.section || 'all'; + } + + isScheduledSection(section) { + const s = section || this.getCurrentSection(); + return s === 'scheduled' || s === 'scheduled-jobs'; + } + + loadData(section) { + const currentSection = section || this.getCurrentSection(); + this.setState({ + loading: true, + errorMessage: '', + hasPermission: true, + ...(currentSection === 'status' ? { jobStatus: undefined } : {}), + }); + this.props.jobs.dispatch(ActionTypes.FETCH, { section: currentSection }).finally(() => { const err = this.props.jobs.data && this.props.jobs.data.get('err') // Verify error message, used to control collaborators permissions if (err && err.code === 403) @@ -153,13 +189,14 @@ class Jobs extends TableView { } // If is a unexpected error just finish loading state else {this.setState({ loading: false });} - this.renderEmpty() - }); - this.context.getJobStatus(0, this.JOB_STATUS_PAGE_SIZE).then(status => { - this.setState({ jobStatus: status, jobStatusHasMore: status.length === this.JOB_STATUS_PAGE_SIZE }); - }).catch(() => { - this.setState({ jobStatus: [], jobStatusHasMore: false }); }); + if (currentSection === 'status') { + this.context.getJobStatus(0, this.JOB_STATUS_PAGE_SIZE).then(status => { + this.setState({ jobStatus: status, jobStatusHasMore: status.length === this.JOB_STATUS_PAGE_SIZE }); + }).catch(() => { + this.setState({ jobStatus: [], jobStatusHasMore: false }); + }); + } } loadMoreJobStatus() { @@ -191,7 +228,7 @@ class Jobs extends TableView { linkPrefix={'jobs/'} categories={[ { name: 'All Jobs', id: 'all' }, - // { name: 'Scheduled Jobs', id: 'scheduled' }, + { name: 'Scheduled Jobs', id: 'scheduled-jobs' }, { name: 'Job Status', id: 'status' }, ]} /> @@ -210,21 +247,59 @@ class Jobs extends TableView { ); - } else if (this.props.params.section === 'scheduled') { + } else if (this.isScheduledSection()) { return ( {data.description} {data.jobName} {scheduleString(data)} - - + + ); @@ -259,7 +334,7 @@ class Jobs extends TableView { Actions , ]; - } else if (this.props.params.section === 'scheduled') { + } else if (this.isScheduledSection()) { return [ Name @@ -296,9 +371,6 @@ class Jobs extends TableView { } renderFooter() { - if (this.props.params.section === 'scheduled') { - return ; - } if (this.props.params.section === 'status' && this.state.jobStatus && this.state.jobStatus.length > 0) { const { jobStatusHasMore, jobStatusLoadingMore } = this.state; return ( @@ -334,17 +406,11 @@ class Jobs extends TableView { description="Define Jobs on parse-server with Parse.Cloud.job()" /> ); - } else if (this.props.params.section === 'scheduled') { + } else if (this.isScheduledSection()) { return ( -

{'On this page you can create JobSchedule objects.'}

-
- - - } + description="There are no scheduled jobs to show at this time." /> ); } else { @@ -358,33 +424,86 @@ class Jobs extends TableView { } renderExtras() { - if (this.state.toDelete) { + const { toDelete, deleteInProgress, deleteError, toEdit, toCreate } = this.state; + if (toCreate) { return ( - this.setState({ toDelete: null })} - onConfirm={() => { - this.setState({ toDelete: null }); - this.props.jobs.dispatch(ActionTypes.DELETE, { - jobId: this.state.toDelete, - }); + this.setState({ toCreate: false })} + onSuccess={() => { + this.setState({ toCreate: false }); + this.loadData(); + }} + /> + ); + } + if (toEdit) { + return ( + this.setState({ toEdit: null })} + onSuccess={() => { + this.setState({ toEdit: null }); + this.loadData(); }} /> ); } + if (!toDelete) { + return null; + } + return ( + this.setState({ toDelete: null, deleteError: null })} + onConfirm={() => { + this.setState({ deleteInProgress: true, deleteError: null }); + this.context.deleteScheduledJob(toDelete.objectId) + .then(() => { + this.setState({ toDelete: null, deleteInProgress: false, deleteError: null }); + this.loadData(); + }) + .catch(err => { + this.setState({ + deleteInProgress: false, + deleteError: (err && err.error) || 'Failed to delete job. Please try again.', + }); + }); + }} + > + {deleteError ? ( + {deleteError} + ) : null} + + ); } tableData() { // Return a empty array if user don't have permission to read scheduled jobs if (!this.state.hasPermission) {return []} + const currentSection = this.getCurrentSection(); + const jobsState = this.props.jobs.data; + if (jobsState) { + const fetchedSection = jobsState.get('section'); + // While loading data for the new section, return undefined so TableView shows + // the loader instead of the empty state. + if (fetchedSection && fetchedSection !== currentSection) { + return undefined; + } + } let data = undefined; - if (this.props.params.section === 'scheduled' || this.props.params.section === 'all') { - if (this.props.jobs.data) { - const jobs = this.props.jobs.data.get('jobs'); + if (this.isScheduledSection() || currentSection === 'all') { + if (jobsState) { + const jobs = jobsState.get('jobs'); if (jobs) { if (Array.isArray(jobs)) { data = jobs; @@ -534,9 +653,17 @@ class Jobs extends TableView { - {this.props.availableJobs && this.props.availableJobs.length > 0 ? ( - + ) : (this.props.availableJobs && this.props.availableJobs.length > 0 ? ( + + ) : null)} ); } diff --git a/src/dashboard/Data/Jobs/Jobs.scss b/src/dashboard/Data/Jobs/Jobs.scss index 0fab97298b..7e9b65bf32 100644 --- a/src/dashboard/Data/Jobs/Jobs.scss +++ b/src/dashboard/Data/Jobs/Jobs.scss @@ -18,3 +18,31 @@ margin-right: 10px; } } + +.scheduleJobButton { + margin-left: 12px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 14px; + height: 32px; + box-sizing: border-box; + background: rgba(39, 174, 96, 0.06); + border: 1px solid rgba(39, 174, 96, 0.6); + border-radius: 6px; + color: #27AE60; + font-size: 13px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; + + &:hover { + background: rgba(39, 174, 96, 0.14); + border-color: #27AE60; + } + + &:active { + background: rgba(39, 174, 96, 0.22); + } +} diff --git a/src/lib/ParseApp.js b/src/lib/ParseApp.js index b0f81e432f..ced71c7e5b 100644 --- a/src/lib/ParseApp.js +++ b/src/lib/ParseApp.js @@ -1057,6 +1057,26 @@ export default class ParseApp { return this.apiRequest('GET', path, {}, { useMasterKey: true }); } + getScheduledJobs() { + // eslint-disable-next-line no-undef + return axios.get(`${b4aSettings.BACK4APP_API_PATH}/jobs/${this.slug}`, { withCredentials: true }).then(({ data }) => data); + } + + deleteScheduledJob(jobId) { + // eslint-disable-next-line no-undef + return axios.delete(`${b4aSettings.BACK4APP_API_PATH}/jobs/delete/${jobId}/`, { withCredentials: true }).then(({ data }) => data); + } + + updateScheduledJob(jobId, payload) { + // eslint-disable-next-line no-undef + return axios.post(`${b4aSettings.BACK4APP_API_PATH}/jobs/job/${jobId}/`, payload, { withCredentials: true }).then(({ data }) => data); + } + + createScheduledJob(payload) { + // eslint-disable-next-line no-undef + return axios.post(`${b4aSettings.BACK4APP_API_PATH}/jobs/${this.slug}/`, payload, { withCredentials: true }).then(({ data }) => data); + } + getJobStatus(skip = 0, limit = 100) { const query = new Parse.Query('_JobStatus'); query.descending('createdAt'); diff --git a/src/lib/stores/JobsStore.js b/src/lib/stores/JobsStore.js index 1bcb47a092..109598e26f 100644 --- a/src/lib/stores/JobsStore.js +++ b/src/lib/stores/JobsStore.js @@ -20,28 +20,59 @@ function JobsStore(state, action) { let path = ''; switch (action.type) { case ActionTypes.FETCH: - if (state && new Date() - state.get('lastFetch') < 60000) { + const isScheduledJobsSection = action.section === 'scheduled-jobs'; + if ( + state && + state.get('section') === action.section && + !isScheduledJobsSection && + new Date() - state.get('lastFetch') < 60000 + ) { return Promise.resolve(state); } + if (isScheduledJobsSection) { + return action.app.getScheduledJobs().then( + results => { + const jobs = (results.jobs || []).map(job => ({ + objectId: job.id || job._id, + description: job.name || job.description || job.jobName || job.cloudCodeFunction || '-', + jobName: job.cloudCodeFunction || job.jobName || '-', + startAfter: ( + (job.schedule && (job.schedule.start || job.schedule.startAfter || job.schedule.startAt)) || + job.startAfter || + job.startAt + ), + repeatMinutes: job.schedule && job.schedule.intervalRun, + timeOfDay: (job.schedule && job.schedule.timeOfDay) || null, + params: job.parameter ? JSON.stringify(job.parameter) : null, + })); + return Map({ lastFetch: new Date(), section: action.section, jobs: List(jobs) }); + }, + err => Map({ lastFetch: new Date(), section: action.section, jobs: [], err }) + ); + } return Parse._request('GET', 'serverInfo', {}).then(serverInfo => { - let serverVersionPrefix = serverInfo.parseServerVersion.substring(0,3) + const serverVersionPrefix = serverInfo.parseServerVersion.substring(0,3) if (serverVersionPrefix === '2.2' || serverVersionPrefix === '2.3') { path = 'cloud_code/jobs?per_page=50' return Parse._request('GET', path, {}, { useMasterKey: true}).then((results) => { - return Map({ lastFetch: new Date(), jobs: List(results) }); + return Map({ lastFetch: new Date(), section: action.section, jobs: List(results) }); }) // In error case return a map with a empty array and the error message // used to control collaborators permissions - .catch(err => Map({ lastFetch: new Date(), jobs: [], err })); + .catch(err => Map({ lastFetch: new Date(), section: action.section, jobs: [], err })); } else { path = 'cloud_code/jobs/data?per_page=50'; return Parse._request('GET', path, {}, { useMasterKey: true}).then((results) => { - return Map({ lastFetch: new Date(), jobs: List(results.jobs.map(job => ({ 'jobName': job })))}); + return Map({ + lastFetch: new Date(), + section: action.section, + jobs: List(results.jobs.map(job => ({ 'jobName': job }))) + }); }) // In error case return a map with a empty array and the error message // used to control collaborators permissions - .catch(err => Map({ lastFetch: new Date(), jobs: [], err })); + .catch(err => Map({ lastFetch: new Date(), section: action.section, jobs: [], err })); } }) From f01245878308f8dd36574bca7e6f2f25d6f332fe Mon Sep 17 00:00:00 2001 From: sadakchap Date: Fri, 6 Mar 2026 05:14:28 +0530 Subject: [PATCH 2/5] fix schedule field --- src/lib/stores/JobsStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/stores/JobsStore.js b/src/lib/stores/JobsStore.js index 109598e26f..dbe7aef6ac 100644 --- a/src/lib/stores/JobsStore.js +++ b/src/lib/stores/JobsStore.js @@ -42,7 +42,7 @@ function JobsStore(state, action) { job.startAt ), repeatMinutes: job.schedule && job.schedule.intervalRun, - timeOfDay: (job.schedule && job.schedule.timeOfDay) || null, + timeOfDay: (job.schedule && (job.schedule.dailyRun || job.schedule.timeOfDay)) || null, params: job.parameter ? JSON.stringify(job.parameter) : null, })); return Map({ lastFetch: new Date(), section: action.section, jobs: List(jobs) }); From ed7529fbe20433f3898fb1a54bcb0f1d5397d24f Mon Sep 17 00:00:00 2001 From: sadakchap Date: Fri, 6 Mar 2026 05:23:49 +0530 Subject: [PATCH 3/5] fix correct field --- src/lib/stores/JobsStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/stores/JobsStore.js b/src/lib/stores/JobsStore.js index dbe7aef6ac..020b1af0d2 100644 --- a/src/lib/stores/JobsStore.js +++ b/src/lib/stores/JobsStore.js @@ -41,7 +41,7 @@ function JobsStore(state, action) { job.startAfter || job.startAt ), - repeatMinutes: job.schedule && job.schedule.intervalRun, + repeatMinutes: job.schedule && (job.schedule.intervalRun || (job.schedule.dailyRun ? 1440 : null)), timeOfDay: (job.schedule && (job.schedule.dailyRun || job.schedule.timeOfDay)) || null, params: job.parameter ? JSON.stringify(job.parameter) : null, })); From 4ccc5c38d848dce4311e5bff1a266e76eef70f5b Mon Sep 17 00:00:00 2001 From: sadakchap Date: Fri, 6 Mar 2026 05:38:17 +0530 Subject: [PATCH 4/5] add plan limit checks --- src/dashboard/Data/Jobs/Jobs.react.js | 61 ++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/src/dashboard/Data/Jobs/Jobs.react.js b/src/dashboard/Data/Jobs/Jobs.react.js index e395d8b90a..b764d38984 100644 --- a/src/dashboard/Data/Jobs/Jobs.react.js +++ b/src/dashboard/Data/Jobs/Jobs.react.js @@ -109,11 +109,29 @@ class Jobs extends TableView { // Job Status pagination (botão "Carregar mais") jobStatusHasMore: true, jobStatusLoadingMore: false, + // Job limit enforcement + jobLimitReached: false, + maxJobAmount: undefined, }; this.filterWrapRef = React.createRef(); this.JOB_STATUS_PAGE_SIZE = 100; } + handleScheduleClick() { + const { maxJobAmount } = this.state; + if (maxJobAmount === undefined) { + // Plan data not yet loaded — proceed and let the backend enforce the limit + this.setState({ toCreate: true }); + return; + } + const currentCount = (this.tableData() || []).length; + if (currentCount >= maxJobAmount) { + this.setState({ jobLimitReached: true }); + } else { + this.setState({ toCreate: true }); + } + } + componentWillMount() { this.loadData(); } @@ -169,8 +187,21 @@ class Jobs extends TableView { loading: true, errorMessage: '', hasPermission: true, + jobLimitReached: false, + ...(this.isScheduledSection(currentSection) ? { maxJobAmount: undefined } : {}), ...(currentSection === 'status' ? { jobStatus: undefined } : {}), }); + + if (this.isScheduledSection(currentSection)) { + this.context.getAppPlanData() + .then(planData => { + this.setState({ maxJobAmount: (planData && planData.maxJobAmount) || null }); + }) + .catch(() => { + this.setState({ maxJobAmount: null }); + }); + } + this.props.jobs.dispatch(ActionTypes.FETCH, { section: currentSection }).finally(() => { const err = this.props.jobs.data && this.props.jobs.data.get('err') // Verify error message, used to control collaborators permissions @@ -424,7 +455,29 @@ class Jobs extends TableView { } renderExtras() { - const { toDelete, deleteInProgress, deleteError, toEdit, toCreate } = this.state; + const { toDelete, deleteInProgress, deleteError, toEdit, toCreate, jobLimitReached } = this.state; + + if (jobLimitReached) { + return ( + this.setState({ jobLimitReached: false })} + onConfirm={() => { + this.props.navigate(generatePath(this.context, 'plan-usage')); + }} + > +
+ Upgrade your plan to schedule additional background jobs. +
+
+ ); + } + if (toCreate) { return ( {this.isScheduledSection() ? ( - From 5bffabfc0348036dd55ed91d74def6b4efa5cc03 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Tue, 10 Mar 2026 12:01:49 +0530 Subject: [PATCH 5/5] fix error msg placement & add validation --- .../Data/Jobs/EditScheduledJobModal.react.js | 8 +- src/dashboard/Data/Jobs/Jobs.react.js | 100 +++++++----------- 2 files changed, 40 insertions(+), 68 deletions(-) diff --git a/src/dashboard/Data/Jobs/EditScheduledJobModal.react.js b/src/dashboard/Data/Jobs/EditScheduledJobModal.react.js index 278cc4f8c0..8ae97f2d92 100644 --- a/src/dashboard/Data/Jobs/EditScheduledJobModal.react.js +++ b/src/dashboard/Data/Jobs/EditScheduledJobModal.react.js @@ -203,7 +203,7 @@ const EditScheduledJobModal = ({ job, context, onCancel, onSuccess }) => { subtitle={isCreate ? 'Configure a new scheduled job' : 'Update the job configuration below'} confirmText={saving ? (isCreate ? 'Scheduling...' : 'Saving...') : (isCreate ? 'Schedule' : 'Save')} cancelText="Cancel" - disableConfirm={saving || loadingJobs} + disableConfirm={saving || loadingJobs || name.trim().length < 3} disableCancel={saving} buttonsInCenter={false} width={700} @@ -362,10 +362,10 @@ const EditScheduledJobModal = ({ job, context, onCancel, onSuccess }) => { /> )} - {error && ( - {error} - )} + {error && ( + {error} + )} ); }; diff --git a/src/dashboard/Data/Jobs/Jobs.react.js b/src/dashboard/Data/Jobs/Jobs.react.js index b764d38984..7288f6433b 100644 --- a/src/dashboard/Data/Jobs/Jobs.react.js +++ b/src/dashboard/Data/Jobs/Jobs.react.js @@ -279,58 +279,22 @@ class Jobs extends TableView { ); } else if (this.isScheduledSection()) { + const openEdit = () => this.setState({ toEdit: data }); + const openDelete = () => this.setState({ toDelete: data, deleteError: null }); return ( - {data.description} - {data.jobName} - {scheduleString(data)} - -
+ {data.description} + {data.jobName} + {scheduleString(data)} + + e.stopPropagation()}> -
- - -
+
+ + + + + ); @@ -367,16 +331,16 @@ class Jobs extends TableView { ]; } else if (this.isScheduledSection()) { return [ - + Name , - + Function , - + Schedule (UTC) , - + Actions , ]; @@ -707,19 +671,27 @@ class Jobs extends TableView { {this.isScheduledSection() ? ( - + /> ) : (this.props.availableJobs && this.props.availableJobs.length > 0 ? ( - +