Skip to content
Open
3 changes: 2 additions & 1 deletion components/webfield/BaseMenuBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const BaseMenuBar = ({
querySearchInfoModal,
searchPlaceHolder,
extraClasses,
uniqueIdentifier = 'note.id',
enablePDFDownload = false,
}) => {
const disabledMessageButton = selectedIds?.length === 0
Expand Down Expand Up @@ -69,7 +70,7 @@ const BaseMenuBar = ({
cleanImmediateSearchTerm.slice(1),
filterOperators,
propertiesAllowed,
'note.id'
uniqueIdentifier
)
if (queryIsInvalid) {
setQueryIsInvalidStatus(true)
Expand Down
10 changes: 8 additions & 2 deletions components/webfield/ProgramChairConsole.js
Original file line number Diff line number Diff line change
Expand Up @@ -541,14 +541,20 @@ return officialReviews.length;

/**
* @name ProgramChairConsoleConfig.sacStatuspropertiesAllowed
* @description Query-search properties override for SAC status (direct paper assignment mode).
* @description Query-search properties override for SAC status (direct paper assignment mode), it also support function string
* @type {Object}
* @default built-in SAC defaults
* @example
* {
* "sacStatuspropertiesAllowed": {
* "number": ["number"],
* "name": ["sacProfile.preferredName"],
* "email": ["sacProfile.preferredEmail"]
* "email": ["sacProfile.preferredEmail"],
* "hasSubmissionWithFewerThan3Reviews": `
* const assignedNotes = row.notes
* const hasSubmission = assignedNotes.some(note => (note.officialReviews?.length ?? 0) < 3)
* return hasSubmission ? 'yes' : 'no'
* `
* }
* }
*/
Expand Down
49 changes: 43 additions & 6 deletions components/webfield/ProgramChairConsole/AreaChairStatusMenuBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,47 @@ const AreaChairStatusMenuBar = ({
reviewerName,
} = useContext(WebFieldContext)
const filterOperators = filterOperatorsConfig ?? ['!=', '>=', '<=', '>', '<', '==', '=']
const propertiesAllowed = areaChairStatusPropertiesAllowed ?? {
number: ['number'],
name: ['areaChairProfile.preferredName'],
[camelCase(seniorAreaChairName)]: ['seniorAreaChair.seniorAreaChairId'],
}
const propertiesAllowed = areaChairStatusPropertiesAllowed
? Object.fromEntries(
Object.entries(areaChairStatusPropertiesAllowed).map(([key, value]) => {
if (typeof value === 'string') {
return [key, [key]]
}
return [key, value]
})
)
: {
number: ['number'],
name: ['areaChairProfile.preferredName'],
[camelCase(seniorAreaChairName)]: ['seniorAreaChair.seniorAreaChairId'],
}

const functionExtraProperties = (() => {
if (typeof areaChairStatusPropertiesAllowed !== 'object') return {}
const result = {}
Object.entries(areaChairStatusPropertiesAllowed).forEach(([key, value]) => {
if (Array.isArray(value)) return
try {
result[key] = Function('row', value)
} catch (error) {
// oxlint-disable-next-line no-console
console.error(`Error parsing function for extra property ${key}: ${error}`)
}
})
return result
})()

const tableRowsAllWithFilterProperties =
Object.keys(functionExtraProperties).length > 0
? tableRowsAll.map((row) => {
const extraProperties = {}
for (const [key, value] of Object.entries(functionExtraProperties)) {
extraProperties[key] = value(row)
}
return { ...row, ...extraProperties }
})
: tableRowsAll

const messageAreaChairOptions = [
...(bidEnabled
? [
Expand Down Expand Up @@ -352,7 +388,7 @@ const AreaChairStatusMenuBar = ({

return (
<BaseMenuBar
tableRowsAll={tableRowsAll}
tableRowsAll={tableRowsAllWithFilterProperties}
tableRows={tableRows}
selectedIds={selectedAreaChairIds}
setSelectedIds={setSelectedAreaChairIds}
Expand All @@ -373,6 +409,7 @@ const AreaChairStatusMenuBar = ({
messageModal={(props) => <MessageACSACModal {...props} />}
querySearchInfoModal={(props) => <QuerySearchInfoModal {...props} />}
extraClasses="ac-status-menu"
uniqueIdentifier="areaChairProfileId"
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useContext } from 'react'
import BaseMenuBar from '../BaseMenuBar'
import WebFieldContext from '../../WebFieldContext'
import { pluralizeString, prettyField } from '../../../lib/utils'
import WebFieldContext from '../../WebFieldContext'
import BaseMenuBar from '../BaseMenuBar'
import QuerySearchInfoModal from '../QuerySearchInfoModal'
import { MessageACSACModal } from './AreaChairStatusMenuBar'

Expand All @@ -25,11 +25,47 @@ const SeniorAreaChairStatusMenuBarForDirectPaperAssignment = ({
submissionName,
} = useContext(WebFieldContext)
const filterOperators = filterOperatorsConfig ?? ['!=', '>=', '<=', '>', '<', '==', '=']
const propertiesAllowed = propertiesAllowedConfig ?? {
number: ['number'],
name: ['sacProfile.preferredName'],
email: ['areaChairProfile.preferredEmail'],
}
const propertiesAllowed = propertiesAllowedConfig
? Object.fromEntries(
Object.entries(propertiesAllowedConfig).map(([key, value]) => {
if (typeof value === 'string') {
return [key, [key]]
}
return [key, value]
})
)
: {
number: ['number'],
name: ['sacProfile.preferredName'],
email: ['areaChairProfile.preferredEmail'],
}

const functionExtraProperties = (() => {
if (typeof propertiesAllowedConfig !== 'object') return {}
const result = {}
Object.entries(propertiesAllowedConfig).forEach(([key, value]) => {
if (Array.isArray(value)) return
try {
result[key] = Function('row', value)
} catch (error) {
// oxlint-disable-next-line no-console
console.error(`Error parsing function for extra property ${key}: ${error}`)
}
})
return result
})()

const tableRowsAllWithFilterProperties =
Object.keys(functionExtraProperties).length > 0
? tableRowsAll.map((row) => {
const extraProperties = {}
for (const [key, value] of Object.entries(functionExtraProperties)) {
extraProperties[key] = value(row)
}
return { ...row, ...extraProperties }
})
: tableRowsAll

const messageSeniorAreaChairOptions = [
{
label: `${prettyField(seniorAreaChairName)} with unsubmitted ${pluralizeString(
Expand Down Expand Up @@ -129,7 +165,7 @@ const SeniorAreaChairStatusMenuBarForDirectPaperAssignment = ({

return (
<BaseMenuBar
tableRowsAll={tableRowsAll}
tableRowsAll={tableRowsAllWithFilterProperties}
tableRows={tableRows}
setData={setSeniorAreaChairStatusTabData}
shortPhrase={shortPhrase}
Expand Down
98 changes: 98 additions & 0 deletions unitTests/AreaChairStatusMenuBar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import { renderWithWebFieldContext } from './util'
import AreaChairStatusMenuBar from '../components/webfield/ProgramChairConsole/AreaChairStatusMenuBar'

let baseMenuBarProps

jest.mock('nanoid', () => ({ nanoid: () => 'some id' }))
jest.mock('../components/webfield/BaseMenuBar', () => (props) => {
baseMenuBarProps = props
return <span>Base Menu Bar</span>
})

beforeEach(() => {
baseMenuBarProps = null
})

describe('AreaChairStatusMenuBar', () => {
test('use default query search param when areaChairStatusPropertiesAllowed is not defined', () => {
const providerProps = {
value: {
officialReviewName: 'Offical_Review',
areaChairStatusPropertiesAllowed: undefined,
},
}
const componentProps = {
reviewRatingName: 'rating',
tableRowsAll: [
{
areaChairProfile: { id: '~Area_Chair1' },
notes: [{ noteNumber: 1 }, { noteNumber: 2 }, { noteNumber: 3 }],
},
{
areaChairProfile: { id: '~Area_Chair2' },
notes: [{ noteNumber: 4 }, { noteNumber: 5 }, { noteNumber: 6 }],
},
],
}

renderWithWebFieldContext(<AreaChairStatusMenuBar {...componentProps} />, providerProps)

expect(baseMenuBarProps.propertiesAllowed.number).toEqual(['number'])
expect(baseMenuBarProps.propertiesAllowed.name).toEqual(['areaChairProfile.preferredName'])
expect(baseMenuBarProps.propertiesAllowed.seniorAreaChairs).toEqual([
'seniorAreaChair.seniorAreaChairId',
])
})

test('allow query search param to be overwritten by areaChairStatusPropertiesAllowed', () => {
const providerProps = {
value: {
officialReviewName: 'Offical_Review',
areaChairStatusPropertiesAllowed: {
number: ['number'],
numTotalReplyCount: `
const notesAssigned = row.notes
const replyCounts = notesAssigned.map(note => note.replyCount ?? 0)
const totalReplyCount = replyCounts.reduce((sum, count) => sum + count, 0)
return totalReplyCount
`,
},
},
}
const componentProps = {
reviewRatingName: 'rating',
tableRowsAll: [
{
areaChairProfile: { id: '~Area_Chair1' },
notes: [
{ noteNumber: 1, replyCount: 3 },
{ noteNumber: 2, replyCount: 3 },
{ noteNumber: 3, replyCount: 3 },
],
},
{
areaChairProfile: { id: '~Area_Chair2' },
notes: [
{ noteNumber: 4, replyCount: 3 },
{ noteNumber: 5, replyCount: 4 },
{ noteNumber: 6, replyCount: 5 },
],
},
],
}

renderWithWebFieldContext(<AreaChairStatusMenuBar {...componentProps} />, providerProps)

expect(baseMenuBarProps.propertiesAllowed.number).toEqual(['number'])
expect(baseMenuBarProps.propertiesAllowed.numTotalReplyCount).toEqual([
'numTotalReplyCount',
])

expect(baseMenuBarProps.tableRowsAll[0].numTotalReplyCount).toEqual(9)
expect(baseMenuBarProps.tableRowsAll[1].numTotalReplyCount).toEqual(12)

expect(baseMenuBarProps.uniqueIdentifier).toEqual('areaChairProfileId') // is note.id by default
})
})