Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/actions/payments.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
* @param {String|Number} memberId
* @param {String} memberHandle
* @param {String} paymentTitle
* @param {String} description
* @param {String|Number} amount
* @param {String|Number} billingAccountId
*/
Expand All @@ -22,6 +23,7 @@ export function createMemberPayment (
memberId,
memberHandle,
paymentTitle,
description,
amount,
billingAccountId
) {
Expand All @@ -31,13 +33,14 @@ export function createMemberPayment (
})

const parsedAmount = Number(amount)
const trimmedDescription = typeof description === 'string' ? description.trim() : ''
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The trimmedDescription variable is assigned a trimmed version of description only if it is a string. If description is not a string, it defaults to an empty string. Consider handling non-string values more explicitly, or validating the input type earlier to ensure description is always a string.

const payload = {
winnerId: String(memberId),
type: 'PAYMENT',
origin: 'Topcoder',
category: 'ENGAGEMENT_PAYMENT',
title: paymentTitle,
description: paymentTitle,
description: trimmedDescription,
externalId: String(assignmentId),
attributes: {
memberHandle,
Expand Down
19 changes: 16 additions & 3 deletions src/components/ApplicationsList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,26 @@ const STATUS_UPDATE_OPTIONS = STATUS_OPTIONS.filter(option => option.value !== '
const INPUT_DATE_FORMAT = 'MM/dd/yyyy'
const INPUT_TIME_FORMAT = 'HH:mm'

const ANTICIPATED_START_LABELS = {
IMMEDIATE: 'Immediate',
FEW_DAYS: 'In a few days',
FEW_WEEKS: 'In a few weeks'
}

const formatDateTime = (value) => {
if (!value) {
return '-'
}
return moment(value).format('MMM DD, YYYY HH:mm')
}

const formatAnticipatedStart = (value) => {
if (!value) {
return '-'
}
return ANTICIPATED_START_LABELS[value] || value
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider adding validation to ensure value is one of the expected keys in ANTICIPATED_START_LABELS before using it as a fallback. This will prevent unexpected behavior if value is not a recognized key.

}

const getStatusClass = (status) => {
const normalized = (status || '').toString().toLowerCase().replace(/\s+/g, '_')
if (normalized === 'submitted') {
Expand Down Expand Up @@ -286,8 +299,8 @@ const ApplicationsList = ({
<span>{engagement && engagement.status ? engagement.status : '-'}</span>
</div>
<div className={styles.metaItem}>
<span className={styles.metaLabel}>Application Deadline:</span>
<span>{formatDateTime(engagement && engagement.applicationDeadline)}</span>
<span className={styles.metaLabel}>Anticipated Start:</span>
<span>{formatAnticipatedStart(engagement && engagement.anticipatedStart)}</span>
</div>
</div>
</div>
Expand Down Expand Up @@ -387,7 +400,7 @@ ApplicationsList.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
title: PropTypes.string,
description: PropTypes.string,
applicationDeadline: PropTypes.any,
anticipatedStart: PropTypes.string,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Ensure that anticipatedStart is always a string that matches one of the keys in ANTICIPATED_START_LABELS. Consider using a more specific PropType or validation to enforce this constraint.

assignedMembers: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
),
Expand Down
63 changes: 31 additions & 32 deletions src/components/EngagementEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import moment from 'moment-timezone'
import cn from 'classnames'
import { PrimaryButton, OutlineButton } from '../Buttons'
import DescriptionField from './DescriptionField'
import DateInput from '../DateInput'
import Select from '../Select'
import SkillsField from '../ChallengeEditor/SkillsField'
import ConfirmationModal from '../Modal/ConfirmationModal'
Expand All @@ -16,8 +15,11 @@ import { formatTimeZoneLabel, formatTimeZoneList } from '../../util/timezones'
import styles from './EngagementEditor.module.scss'

const ANY_OPTION = { label: 'Any', value: 'Any' }
const INPUT_DATE_FORMAT = 'MM/dd/yyyy'
const INPUT_TIME_FORMAT = 'HH:mm'
const ANTICIPATED_START_OPTIONS = [
{ label: 'Immediate', value: 'Immediate' },
{ label: 'In a few days', value: 'In a few days' },
{ label: 'In a few weeks', value: 'In a few weeks' }
]

const getEmptyEngagement = () => ({
title: '',
Expand All @@ -29,6 +31,7 @@ const getEmptyEngagement = () => ({
role: null,
workload: null,
compensationRange: '',
anticipatedStart: null,
status: 'Open',
isPrivate: false,
requiredMemberCount: '',
Expand Down Expand Up @@ -78,7 +81,6 @@ const EngagementEditor = ({
onUpdateInput,
onUpdateDescription,
onUpdateSkills,
onUpdateDate,
onSavePublish,
onCancel,
onDelete,
Expand Down Expand Up @@ -169,17 +171,6 @@ const EngagementEditor = ({
const selectedCountries = (engagement.countries || []).map(code => {
return countryOptionsByValue[code] || { label: code, value: code }
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The function getMinApplicationDeadline is removed, but it might still be useful if there's a need to validate or set a minimum date for any future date-related fields. Consider keeping it if there's a possibility of reusing it.

const getMinApplicationDeadline = () => {
return moment().add(1, 'minute').startOf('minute').toDate()
}
const isValidApplicationDeadlineDate = (currentDate) => {
if (!currentDate) {
return false
}
const minDateTime = getMinApplicationDeadline()
return moment(currentDate).isSameOrAfter(minDateTime, 'day')
}

const selectedRoleOption = useMemo(() => {
if (!engagement.role) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The function isValidApplicationDeadlineDate is removed. Ensure that any date validation logic that was previously handled by this function is not needed elsewhere, or consider implementing a similar validation if necessary for future date fields.

return null
Expand All @@ -194,8 +185,16 @@ const EngagementEditor = ({
return JOB_WORKLOAD_OPTIONS.find(option => option.value === engagement.workload || option.label === engagement.workload) || null
}, [engagement.workload])

const selectedAnticipatedStartOption = useMemo(() => {
if (!engagement.anticipatedStart) {
return null
}
return ANTICIPATED_START_OPTIONS.find(option => option.value === engagement.anticipatedStart || option.label === engagement.anticipatedStart) || null
}, [engagement.anticipatedStart])

const roleLabel = selectedRoleOption ? selectedRoleOption.label : engagement.role
const workloadLabel = selectedWorkloadOption ? selectedWorkloadOption.label : engagement.workload
const anticipatedStartLabel = selectedAnticipatedStartOption ? selectedAnticipatedStartOption.label : engagement.anticipatedStart
const assignments = Array.isArray(engagement.assignments) ? engagement.assignments : []
const assignedMembers = Array.isArray(engagement.assignedMembers) ? engagement.assignedMembers : []
const assignedMemberHandles = Array.isArray(engagement.assignedMemberHandles) ? engagement.assignedMemberHandles : []
Expand Down Expand Up @@ -572,28 +571,30 @@ const EngagementEditor = ({

<div className={styles.row}>
<div className={cn(styles.field, styles.col1)}>
<label>Application Deadline <span>*</span> :</label>
<label>Anticipated Start <span>*</span> :</label>
</div>
<div className={cn(styles.field, styles.col2)}>
{canEdit ? (
<DateInput
<Select
className={styles.selectInput}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 design]
The Select component for Anticipated Start is set with isClearable={false}. Consider whether users should be able to clear their selection, as this could impact user experience if they need to reset the field.

value={engagement.applicationDeadline}
dateFormat={INPUT_DATE_FORMAT}
timeFormat={INPUT_TIME_FORMAT}
onChange={value => onUpdateDate('applicationDeadline', value)}
isValidDate={isValidApplicationDeadlineDate}
minDateTime={getMinApplicationDeadline}
useBottomBorder
options={ANTICIPATED_START_OPTIONS}
value={selectedAnticipatedStartOption}
onChange={(option) => onUpdateInput({
target: {
name: 'anticipatedStart',
value: option ? option.value : null
}
})}
isClearable={false}
/>
) : (
<div className={styles.readOnlyValue}>
{engagement.applicationDeadline
? moment(engagement.applicationDeadline).format('MMM DD, YYYY HH:mm')
: '-'}
{anticipatedStartLabel || '-'}
</div>
)}
{submitTriggered && validationErrors.applicationDeadline && (
<div className={styles.error}>{validationErrors.applicationDeadline}</div>
{submitTriggered && validationErrors.anticipatedStart && (
<div className={styles.error}>{validationErrors.anticipatedStart}</div>
)}
</div>
</div>
Expand Down Expand Up @@ -776,7 +777,6 @@ EngagementEditor.defaultProps = {
onUpdateInput: () => {},
onUpdateDescription: () => {},
onUpdateSkills: () => {},
onUpdateDate: () => {},
onSavePublish: () => {},
onCancel: () => {},
onDelete: () => {},
Expand Down Expand Up @@ -820,7 +820,7 @@ EngagementEditor.propTypes = {
timezones: PropTypes.arrayOf(PropTypes.string),
countries: PropTypes.arrayOf(PropTypes.string),
skills: PropTypes.arrayOf(PropTypes.shape()),
applicationDeadline: PropTypes.any,
anticipatedStart: PropTypes.string,
status: PropTypes.string
}),
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
Expand All @@ -833,7 +833,7 @@ EngagementEditor.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
durationWeeks: PropTypes.string,
applicationDeadline: PropTypes.string,
anticipatedStart: PropTypes.string,
skills: PropTypes.string,
timezones: PropTypes.string,
countries: PropTypes.string,
Expand All @@ -846,7 +846,6 @@ EngagementEditor.propTypes = {
onUpdateInput: PropTypes.func,
onUpdateDescription: PropTypes.func,
onUpdateSkills: PropTypes.func,
onUpdateDate: PropTypes.func,
onSavePublish: PropTypes.func,
onCancel: PropTypes.func,
onDelete: PropTypes.func,
Expand Down
56 changes: 48 additions & 8 deletions src/components/EngagementsList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const STATUS_OPTIONS = [
]

const SORT_OPTIONS = [
{ label: 'Application Deadline', value: 'deadline' },
{ label: 'Anticipated Start', value: 'anticipatedStart' },
{ label: 'Created Date', value: 'createdAt' }
]

Expand All @@ -31,20 +31,58 @@ const SORT_ORDER_OPTIONS = [

const DEFAULT_STATUS_OPTION = STATUS_OPTIONS.find((option) => option.value === 'Open') || STATUS_OPTIONS[0]

const ANTICIPATED_START_LABELS = {
IMMEDIATE: 'Immediate',
FEW_DAYS: 'In a few days',
FEW_WEEKS: 'In a few weeks'
}

const ANTICIPATED_START_ORDER = {
Immediate: 1,
'In a few days': 2,
'In a few weeks': 3,
...Object.keys(ANTICIPATED_START_LABELS).reduce((acc, key, index) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The ANTICIPATED_START_ORDER object is being constructed with both hardcoded values and dynamically generated values from ANTICIPATED_START_LABELS. This could lead to unexpected behavior if the keys in ANTICIPATED_START_LABELS do not match the hardcoded keys. Consider ensuring consistency between these two structures or using one approach consistently.

acc[key] = index + 1
return acc
}, {})
}

const formatDate = (value) => {
if (!value) {
return '-'
}
return moment(value).format('MMM DD, YYYY')
}

const formatAnticipatedStart = (value) => {
if (!value) {
return '-'
}
return ANTICIPATED_START_LABELS[value] || value
}

const getSortValue = (engagement, sortBy) => {
if (sortBy === 'deadline') {
return engagement.applicationDeadline || engagement.application_deadline || null
if (sortBy === 'anticipatedStart') {
const anticipatedStart = engagement.anticipatedStart || engagement.anticipated_start || null
if (!anticipatedStart) {
return null
}
return ANTICIPATED_START_ORDER[anticipatedStart] || 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The getSortValue function returns 0 when ANTICIPATED_START_ORDER does not contain the anticipatedStart value. This could lead to incorrect sorting behavior if 0 is not a valid or expected sort value. Consider returning null or another distinct value to indicate an unrecognized anticipatedStart value.

}
return engagement.createdAt || engagement.createdOn || engagement.created || null
}

const getSortComparable = (value) => {
if (value == null) {
return null
}
if (typeof value === 'number') {
return value
}
const parsed = new Date(value).getTime()
return Number.isNaN(parsed) ? null : parsed
}

const getDurationLabel = (engagement) => {
if (!engagement) {
return '-'
Expand Down Expand Up @@ -214,9 +252,11 @@ const EngagementsList = ({
const sorted = [...results].sort((a, b) => {
const valueA = getSortValue(a, sortBy.value)
const valueB = getSortValue(b, sortBy.value)
const dateA = valueA ? new Date(valueA).getTime() : 0
const dateB = valueB ? new Date(valueB).getTime() : 0
return sortOrder.value === 'asc' ? dateA - dateB : dateB - dateA
const comparableA = getSortComparable(valueA)
const comparableB = getSortComparable(valueB)
const normalizedA = comparableA == null ? 0 : comparableA
const normalizedB = comparableB == null ? 0 : comparableB
return sortOrder.value === 'asc' ? normalizedA - normalizedB : normalizedB - normalizedA
})
return sorted
}, [engagements, statusFilter, searchText, sortBy, sortOrder])
Expand Down Expand Up @@ -309,7 +349,7 @@ const EngagementsList = ({
<th>Title</th>
<th>Duration</th>
<th>Location</th>
<th>Application Deadline</th>
<th>Anticipated Start</th>
{canManage && <th>Applications</th>}
{canManage && <th>Visibility</th>}
{canManage && <th>Members Required</th>}
Expand Down Expand Up @@ -341,7 +381,7 @@ const EngagementsList = ({
<td>{engagement.title || '-'}</td>
<td>{duration}</td>
<td>{location}</td>
<td>{formatDate(engagement.applicationDeadline)}</td>
<td>{formatAnticipatedStart(engagement.anticipatedStart)}</td>
{canManage && (
<td>
{engagement.id ? (
Expand Down
32 changes: 32 additions & 0 deletions src/components/PaymentForm/PaymentForm.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,38 @@
box-sizing: border-box;
}

.dateInput {
:global(.rdt) {
width: 100%;
}

:global(.form-control) {
width: 100%;
padding: 10px 12px;
border: 1px solid $tc-gray-30;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
}

.weekEndingPreview {
margin-top: 6px;
font-size: 13px;
color: $tc-gray-70;
}

.textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid $tc-gray-30;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
resize: vertical;
min-height: 78px;
}

.selectInput {
width: 100%;
}
Expand Down
Loading
Loading