diff --git a/components/webfield/BaseMenuBar.js b/components/webfield/BaseMenuBar.js index 657c87ee0..e0dc6302f 100644 --- a/components/webfield/BaseMenuBar.js +++ b/components/webfield/BaseMenuBar.js @@ -30,6 +30,7 @@ const BaseMenuBar = ({ querySearchInfoModal, searchPlaceHolder, extraClasses, + uniqueIdentifier = 'note.id', enablePDFDownload = false, }) => { const disabledMessageButton = selectedIds?.length === 0 @@ -69,7 +70,7 @@ const BaseMenuBar = ({ cleanImmediateSearchTerm.slice(1), filterOperators, propertiesAllowed, - 'note.id' + uniqueIdentifier ) if (queryIsInvalid) { setQueryIsInvalidStatus(true) diff --git a/components/webfield/ProgramChairConsole.js b/components/webfield/ProgramChairConsole.js index 2e9e0a3f3..4471cb7aa 100644 --- a/components/webfield/ProgramChairConsole.js +++ b/components/webfield/ProgramChairConsole.js @@ -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' + * ` * } * } */ diff --git a/components/webfield/ProgramChairConsole/AreaChairStatusMenuBar.js b/components/webfield/ProgramChairConsole/AreaChairStatusMenuBar.js index ea7978900..78924da71 100644 --- a/components/webfield/ProgramChairConsole/AreaChairStatusMenuBar.js +++ b/components/webfield/ProgramChairConsole/AreaChairStatusMenuBar.js @@ -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 ? [ @@ -352,7 +388,7 @@ const AreaChairStatusMenuBar = ({ return ( } querySearchInfoModal={(props) => } extraClasses="ac-status-menu" + uniqueIdentifier="areaChairProfileId" /> ) } diff --git a/components/webfield/ProgramChairConsole/SeniorAreaChairStatusMenuBar.js b/components/webfield/ProgramChairConsole/SeniorAreaChairStatusMenuBar.js index 6a92fb76d..43d9c3102 100644 --- a/components/webfield/ProgramChairConsole/SeniorAreaChairStatusMenuBar.js +++ b/components/webfield/ProgramChairConsole/SeniorAreaChairStatusMenuBar.js @@ -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' @@ -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( @@ -129,7 +165,7 @@ const SeniorAreaChairStatusMenuBarForDirectPaperAssignment = ({ return ( ({ nanoid: () => 'some id' })) +jest.mock('../components/webfield/BaseMenuBar', () => (props) => { + baseMenuBarProps = props + return Base Menu Bar +}) + +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(, 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(, 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 + }) +})