From 312b1f2423bfd8c7092ffe5587b4983280133fe4 Mon Sep 17 00:00:00 2001 From: matiasperrone-exo Date: Tue, 7 Apr 2026 21:24:28 +0000 Subject: [PATCH 01/10] fix: Minimum Speaker Requirement not Enforced --- src/components/presentation-speakers-form.js | 57 +++++++++++++------- src/i18n/en.json | 7 ++- src/i18n/zh.json | 10 +++- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/components/presentation-speakers-form.js b/src/components/presentation-speakers-form.js index cbbacec..aa0111a 100644 --- a/src/components/presentation-speakers-form.js +++ b/src/components/presentation-speakers-form.js @@ -18,7 +18,6 @@ import { Exclusive, Dropdown } from 'openstack-uicore-foundation/lib/components' import T from "i18n-react/dist/i18n-react"; import CPFSpeakerInput from './inputs/speaker-input' import Swal from "sweetalert2"; -import {getMarketingValue} from "./marketing-setting"; class PresentationSpeakersForm extends React.Component { constructor(props) { @@ -47,24 +46,41 @@ class PresentationSpeakersForm extends React.Component { } handleSubmit(ev) { - - const entity = {...this.props.entity}; - const { selectionPlanSettings } = this.props; ev.preventDefault(); + const { selectionPlanSettings, entity } = this.props; const validModerator = !entity.type.use_moderator || !entity.type.is_moderator_mandatory || entity.moderator; - const validSpeaker = !entity.type.use_speakers || !entity.type.are_speakers_mandatory || entity.speakers.length > 0; + const presentation = selectionPlanSettings?.CFP_PRESENTATIONS_SINGULAR_LABEL || T.translate("edit_presentation.presentation").toLowerCase(); if (!validModerator) { - Swal.fire("Validation error", T.translate("edit_presentation.errors.add_moderator", - { presentation: selectionPlanSettings?.CFP_PRESENTATIONS_SINGULAR_LABEL || T.translate("edit_presentation.presentation").toLowerCase()}), "warning"); - } else if (!validSpeaker) { - Swal.fire("Validation error", T.translate("edit_presentation.errors.add_speaker", - { presentation: selectionPlanSettings?.CFP_PRESENTATIONS_SINGULAR_LABEL || T.translate("edit_presentation.presentation").toLowerCase(), - speaker: selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker").toLowerCase()}), "warning"); - } else { - this.props.onSubmit(this.props.entity); + Swal.fire("Validation error", T.translate("edit_presentation.errors.add_moderator", { presentation }), "warning"); + return; } + + const MIN = (entity.type.are_speakers_mandatory ? 1 : 0); + const MAX = (entity.type.are_speakers_mandatory || entity.type.use_speakers ? Infinity : 0); + const minSpeakers = entity.type?.min_speakers || MIN; + const maxSpeakers = entity.type?.max_speakers || MAX; + const validSpeaker = !entity.type.use_speakers || (entity.speakers.length <= maxSpeakers && entity.speakers.length >= minSpeakers); + if (!validSpeaker) { + const speaker = (selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker")).toLowerCase(); + const speakers = (selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || T.translate("edit_presentation.speakers")).toLowerCase(); + const TParams = { presentation, speaker, speakers, max: maxSpeakers, min: minSpeakers } + + let errorField = "add_speakers"; + if (Infinity === maxSpeakers) { + errorField = "add_min_number_speakers"; + } + else if (minSpeakers === maxSpeakers ) { + errorField = maxSpeakers === 1 ? "add_only_one_speaker" : "add_exact_number_of_speakers"; + } + + let errorText = T.translate(`edit_presentation.errors.${errorField}`, TParams); + Swal.fire("Validation error", errorText, "warning"); + return; + } + + this.props.onSubmit(this.props.entity); } handleBack(ev) { @@ -101,12 +117,13 @@ class PresentationSpeakersForm extends React.Component { handleAddSpeaker(ev) { const {speaker, currentSpeakerType} = this.state; - const {history, onAddSpeaker, onAddModerator, match} = this.props; + const {history, onAddSpeaker, onAddModerator, match, selectionPlanSettings} = this.props; + const speakerLabel = selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker").toLowerCase(); ev.preventDefault(); if(!speaker){ // speaker not set - this.setState({...this.state, error: T.translate("edit_presentation.errors.missing_speaker")}); + this.setState({...this.state, error: T.translate("edit_presentation.errors.missing_speaker", {speaker: speakerLabel})}); return; } @@ -134,14 +151,14 @@ class PresentationSpeakersForm extends React.Component { } // speaker not set - this.setState({...this.state, error: T.translate("edit_presentation.errors.missing_speaker")}); + this.setState({...this.state, error: T.translate("edit_presentation.errors.missing_speaker", {speaker: speakerLabel})}); return false; } render() { let {summit, selectionPlanSettings, entity, presentation, step} = this.props; let {speakerInput, error, speaker} = this.state; - let eventType = summit.event_types.find(t => t.id == entity.type_id); + let eventType = summit.event_types.find(t => t.id == entity.type_id); let canAddSpeakers = (eventType && eventType.max_speakers > entity.speakers.length); let canAddModerator = (eventType && eventType.max_moderators && !entity.moderator); @@ -156,7 +173,7 @@ class PresentationSpeakersForm extends React.Component { return (
-

{T.translate("edit_presentation.speaker_included", +

{T.translate("edit_presentation.speaker_included", { presentation: selectionPlanSettings?.CFP_PRESENTATIONS_SINGULAR_LABEL || T.translate("edit_presentation.presentation")})}

@@ -220,8 +237,8 @@ class PresentationSpeakersForm extends React.Component { selectionPlanSettings={selectionPlanSettings} value={speakerInput} speakers={entity.speakers} - placeholder={T.translate("edit_presentation.placeholders.speakers", - {speakers: `${selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || + placeholder={T.translate("edit_presentation.placeholders.speakers", + {speakers: `${selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || T.translate('edit_presentation.speakers').toLowerCase()}` })} onChange={this.handleChangeSpeaker} diff --git a/src/i18n/en.json b/src/i18n/en.json index bc37772..ca551a6 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -205,8 +205,11 @@ "errors": { "role": "You must select a Role.", "add_moderator": "You need to add a moderator to the {presentation}.", - "add_speaker": "You need to add at least one {speaker} to the {presentation}.", - "missing_speaker": "You need to select a participant." + "add_speakers": "You need to add between {min} and {max} {speakers} to the {presentation}.", + "add_only_one_speaker": "You need to add one {speaker} to the {presentation}.", + "add_exact_number_of_speakers": "You need to add {min} {speakers} to the {presentation}.", + "add_min_number_speakers": "You need to add at least {min} {speakers} to the {presentation}.", + "missing_speaker": "You need to select a {speaker}." } }, "edit_speaker": { diff --git a/src/i18n/zh.json b/src/i18n/zh.json index 3de4591..67f8191 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -167,12 +167,20 @@ "review_subtitle": "您的演讲已经提交并等待专题负责人审阅", "permission_denied": "没有权限", "no_edit": "您无权编辑此演示文稿", - "add_speaker_error": "您需要添加至少一名演讲者到演讲议题中来。", "chair_comments": "Chair Comments", "presentation_material": "Presentation Material", "placeholders": { "type_id" : "-- Select One --", "level" : "-- Select One --" + }, + "errors": { + "role": "您必须选择一个角色。", + "add_moderator": "您需要添加至少一名主持人到 {presentation} 中来。", + "add_speakers": "您需要添加 {min} 到 {max} 名 {speakers} 到 {presentation} 中来。", + "add_only_one_speaker": "您需要添加一名 {speaker} 到 {presentation} 中来。", + "add_exact_number_of_speakers": "您需要添加 {min} 名 {speakers} 到 {presentation} 中来。", + "add_min_number_speakers": "您需要添加至少 {min} 名 {speakers} 到 {presentation} 中来。", + "missing_speaker": "您需要选择一个 {speaker}。" } }, "edit_speaker": { From 92e553243e2f3742f7f02ea5dfa5120048c0cf0a Mon Sep 17 00:00:00 2001 From: matiasperrone-exo Date: Fri, 10 Apr 2026 19:35:33 +0000 Subject: [PATCH 02/10] chore: Add fixes proposed in PR comments --- src/components/presentation-speakers-form.js | 50 +++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/components/presentation-speakers-form.js b/src/components/presentation-speakers-form.js index aa0111a..c5a0f99 100644 --- a/src/components/presentation-speakers-form.js +++ b/src/components/presentation-speakers-form.js @@ -57,26 +57,41 @@ class PresentationSpeakersForm extends React.Component { return; } - const MIN = (entity.type.are_speakers_mandatory ? 1 : 0); - const MAX = (entity.type.are_speakers_mandatory || entity.type.use_speakers ? Infinity : 0); - const minSpeakers = entity.type?.min_speakers || MIN; - const maxSpeakers = entity.type?.max_speakers || MAX; - const validSpeaker = !entity.type.use_speakers || (entity.speakers.length <= maxSpeakers && entity.speakers.length >= minSpeakers); + const speakersCount = Array.isArray(entity.speakers) ? entity.speakers.length : 0; + const defaultMinSpeakers = (entity.type?.are_speakers_mandatory ? 1 : 0); + const defaultMaxSpeakers = ((entity.type?.are_speakers_mandatory || entity.type?.use_speakers) ? Infinity : 0); + const minSpeakers = entity.type?.min_speakers || defaultMinSpeakers; + const possibleMaxSpeakers = (entity.type?.max_speakers || defaultMaxSpeakers); + // Protection against invalid configuration of max_speakers < min_speakers + const maxSpeakers = possibleMaxSpeakers >= minSpeakers ? possibleMaxSpeakers : minSpeakers; + const validSpeaker = !entity.type.use_speakers || (speakersCount <= maxSpeakers && speakersCount >= minSpeakers); + if (!validSpeaker) { const speaker = (selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker")).toLowerCase(); const speakers = (selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || T.translate("edit_presentation.speakers")).toLowerCase(); - const TParams = { presentation, speaker, speakers, max: maxSpeakers, min: minSpeakers } - - let errorField = "add_speakers"; - if (Infinity === maxSpeakers) { - errorField = "add_min_number_speakers"; - } - else if (minSpeakers === maxSpeakers ) { - errorField = maxSpeakers === 1 ? "add_only_one_speaker" : "add_exact_number_of_speakers"; + const translationParams = { presentation, speaker, speakers, max: maxSpeakers, min: minSpeakers }; + + let errorField; + switch (true) { + // There is no upper limit of speakers but there is a minimum + case (Infinity === maxSpeakers): + errorField = "add_min_number_speakers"; + break; + // There should be only one speaker + case (minSpeakers === maxSpeakers && maxSpeakers === 1): + errorField = "add_only_one_speaker"; + break; + // There should be exactly a number of speakers + case (minSpeakers === maxSpeakers && maxSpeakers !== 1): + errorField = "add_exact_number_of_speakers"; + break; + // The default error message when there is an upper limit and a minimum of speakers + default: + errorField = "add_speakers"; + break; } - let errorText = T.translate(`edit_presentation.errors.${errorField}`, TParams); - Swal.fire("Validation error", errorText, "warning"); + Swal.fire("Validation error", T.translate(`edit_presentation.errors.${errorField}`, translationParams), "warning"); return; } @@ -159,7 +174,8 @@ class PresentationSpeakersForm extends React.Component { let {summit, selectionPlanSettings, entity, presentation, step} = this.props; let {speakerInput, error, speaker} = this.state; let eventType = summit.event_types.find(t => t.id == entity.type_id); - let canAddSpeakers = (eventType && eventType.max_speakers > entity.speakers.length); + let speakersCount = entity.speakers?.length ?? 0; + let canAddSpeakers = (eventType && eventType.max_speakers > speakersCount); let canAddModerator = (eventType && eventType.max_moderators && !entity.moderator); let speakerTypes = []; @@ -203,7 +219,7 @@ class PresentationSpeakersForm extends React.Component {
} - {entity.speakers.map(s => ( + {entity.speakers?.map(s => (
From a6e31adbce190aa68d3d8211c33245254088dd2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Tue, 11 Aug 2026 17:45:09 -0300 Subject: [PATCH 03/10] fix: new error msg for max speakers passed, add getSpeakersLimits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/presentation-speakers-form.js | 203 ++++++++++--------- src/i18n/en.json | 3 +- 2 files changed, 110 insertions(+), 96 deletions(-) diff --git a/src/components/presentation-speakers-form.js b/src/components/presentation-speakers-form.js index c5a0f99..695a815 100644 --- a/src/components/presentation-speakers-form.js +++ b/src/components/presentation-speakers-form.js @@ -19,6 +19,17 @@ import T from "i18n-react/dist/i18n-react"; import CPFSpeakerInput from './inputs/speaker-input' import Swal from "sweetalert2"; +const getSpeakerLimits = (type) => { + if (!type) return { min: 0, max: 0 }; + const defaultMin = type.are_speakers_mandatory ? 1 : 0; + const defaultMax = (type.are_speakers_mandatory || type.use_speakers) ? Infinity : 0; + const min = type.min_speakers ?? defaultMin; + const possibleMax = type.max_speakers ?? defaultMax; + // Protection against invalid configuration of max_speakers < min_speakers + const max = possibleMax >= min ? possibleMax : min; + return { min, max }; +}; + class PresentationSpeakersForm extends React.Component { constructor(props) { super(props); @@ -26,8 +37,8 @@ class PresentationSpeakersForm extends React.Component { this.state = { speaker: {}, speakerInput: null, - entity: {...props.entity}, - currentSpeakerType : null + entity: { ...props.entity }, + currentSpeakerType: null }; this.handleSubmit = this.handleSubmit.bind(this); @@ -39,10 +50,10 @@ class PresentationSpeakersForm extends React.Component { this.handleAddSpeaker = this.handleAddSpeaker.bind(this); } - handleChangeSpeakerType(ev){ - let {value, id} = ev.target; + handleChangeSpeakerType(ev) { + let { value, id } = ev.target; id = id.toString(); - this.setState({...this.state, currentSpeakerType: value, error: null}); + this.setState({ ...this.state, currentSpeakerType: value, error: null }); } handleSubmit(ev) { @@ -58,37 +69,37 @@ class PresentationSpeakersForm extends React.Component { } const speakersCount = Array.isArray(entity.speakers) ? entity.speakers.length : 0; - const defaultMinSpeakers = (entity.type?.are_speakers_mandatory ? 1 : 0); - const defaultMaxSpeakers = ((entity.type?.are_speakers_mandatory || entity.type?.use_speakers) ? Infinity : 0); - const minSpeakers = entity.type?.min_speakers || defaultMinSpeakers; - const possibleMaxSpeakers = (entity.type?.max_speakers || defaultMaxSpeakers); - // Protection against invalid configuration of max_speakers < min_speakers - const maxSpeakers = possibleMaxSpeakers >= minSpeakers ? possibleMaxSpeakers : minSpeakers; + const { min: minSpeakers, max: maxSpeakers } = getSpeakerLimits(entity.type); const validSpeaker = !entity.type.use_speakers || (speakersCount <= maxSpeakers && speakersCount >= minSpeakers); + const excess = speakersCount - maxSpeakers; if (!validSpeaker) { const speaker = (selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker")).toLowerCase(); const speakers = (selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || T.translate("edit_presentation.speakers")).toLowerCase(); - const translationParams = { presentation, speaker, speakers, max: maxSpeakers, min: minSpeakers }; + const translationParams = { presentation, speaker, speakers, max: maxSpeakers, min: minSpeakers, excess }; let errorField; - switch (true) { - // There is no upper limit of speakers but there is a minimum - case (Infinity === maxSpeakers): - errorField = "add_min_number_speakers"; - break; - // There should be only one speaker - case (minSpeakers === maxSpeakers && maxSpeakers === 1): - errorField = "add_only_one_speaker"; - break; - // There should be exactly a number of speakers - case (minSpeakers === maxSpeakers && maxSpeakers !== 1): - errorField = "add_exact_number_of_speakers"; - break; - // The default error message when there is an upper limit and a minimum of speakers - default: - errorField = "add_speakers"; - break; + if (speakersCount > maxSpeakers) { + errorField = "remove_speakers"; + } else { + switch (true) { + // There is no upper limit of speakers but there is a minimum + case (Infinity === maxSpeakers): + errorField = "add_min_number_speakers"; + break; + // There should be only one speaker + case (minSpeakers === maxSpeakers && maxSpeakers === 1): + errorField = "add_only_one_speaker"; + break; + // There should be exactly a number of speakers + case (minSpeakers === maxSpeakers && maxSpeakers !== 1): + errorField = "add_exact_number_of_speakers"; + break; + // The default error message when there is an upper limit and a minimum of speakers + default: + errorField = "add_speakers"; + break; + } } Swal.fire("Validation error", T.translate(`edit_presentation.errors.${errorField}`, translationParams), "warning"); @@ -104,12 +115,12 @@ class PresentationSpeakersForm extends React.Component { } handleChangeSpeaker(ev) { - let {value, id} = ev.target; - this.setState({speaker: value, speakerInput: value}); + let { value, id } = ev.target; + this.setState({ speaker: value, speakerInput: value }); } handleSpeakerClick(speakerId, speakerType, ev) { - let {history, entity, summit} = this.props; + let { history, entity, summit } = this.props; ev.preventDefault(); this.props.onSpeakerEdit(entity.selection_plan_id, entity.id, speakerId, speakerType); } @@ -125,25 +136,25 @@ class PresentationSpeakersForm extends React.Component { } handleEditSpeaker(speakerId, speakerType, ev) { - let {history, entity} = this.props; + let { history, entity } = this.props; ev.preventDefault(); this.props.onSpeakerEdit(entity.selection_plan_id, entity.id, speakerId, speakerType); } handleAddSpeaker(ev) { - const {speaker, currentSpeakerType} = this.state; - const {history, onAddSpeaker, onAddModerator, match, selectionPlanSettings} = this.props; + const { speaker, currentSpeakerType } = this.state; + const { history, onAddSpeaker, onAddModerator, match, selectionPlanSettings } = this.props; const speakerLabel = selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker").toLowerCase(); ev.preventDefault(); - if(!speaker){ + if (!speaker) { // speaker not set - this.setState({...this.state, error: T.translate("edit_presentation.errors.missing_speaker", {speaker: speakerLabel})}); + this.setState({ ...this.state, error: T.translate("edit_presentation.errors.missing_speaker", { speaker: speakerLabel }) }); return; } - if(!currentSpeakerType){ - this.setState({...this.state, error: T.translate("edit_presentation.errors.role")}); + if (!currentSpeakerType) { + this.setState({ ...this.state, error: T.translate("edit_presentation.errors.role") }); return; } @@ -154,7 +165,7 @@ class PresentationSpeakersForm extends React.Component { } else { onAddSpeaker(speaker); } - this.setState({...this.state, currentSpeakerType: null, speakerInput: null, error: null}); + this.setState({ ...this.state, currentSpeakerType: null, speakerInput: null, error: null }); return false; } @@ -166,42 +177,43 @@ class PresentationSpeakersForm extends React.Component { } // speaker not set - this.setState({...this.state, error: T.translate("edit_presentation.errors.missing_speaker", {speaker: speakerLabel})}); + this.setState({ ...this.state, error: T.translate("edit_presentation.errors.missing_speaker", { speaker: speakerLabel }) }); return false; } render() { - let {summit, selectionPlanSettings, entity, presentation, step} = this.props; - let {speakerInput, error, speaker} = this.state; + let { summit, selectionPlanSettings, entity, presentation, step } = this.props; + let { speakerInput, error, speaker } = this.state; let eventType = summit.event_types.find(t => t.id == entity.type_id); let speakersCount = entity.speakers?.length ?? 0; - let canAddSpeakers = (eventType && eventType.max_speakers > speakersCount); + let { max: maxSpeakers } = getSpeakerLimits(eventType); + let canAddSpeakers = (eventType && maxSpeakers > speakersCount); let canAddModerator = (eventType && eventType.max_moderators && !entity.moderator); let speakerTypes = []; - if(canAddSpeakers){ - speakerTypes.push({value:'speaker', label: selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.labels.speaker")}); + if (canAddSpeakers) { + speakerTypes.push({ value: 'speaker', label: selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.labels.speaker") }); } - if(canAddModerator){ - speakerTypes.push({value:'moderator', label: T.translate("edit_presentation.labels.moderator")}); + if (canAddModerator) { + speakerTypes.push({ value: 'moderator', label: T.translate("edit_presentation.labels.moderator") }); } return (

{T.translate("edit_presentation.speaker_included", - { presentation: selectionPlanSettings?.CFP_PRESENTATIONS_SINGULAR_LABEL || T.translate("edit_presentation.presentation")})}

+ { presentation: selectionPlanSettings?.CFP_PRESENTATIONS_SINGULAR_LABEL || T.translate("edit_presentation.presentation") })} - + -
+
{entity.moderator &&
- + {speakerTypes.length > 0 && -
-
-
-
- - -
-
- - -
-
- +
+
+
+
+ + +
+
+ + +
+
+ +
+ {error && +

{error}

} + {speaker && isNaN(speaker.id) && (speaker.value) && +

* Please select a Role and click "Add".

}
- {error && -

{error}

} - {speaker && isNaN(speaker.id) && (speaker.value) && -

* Please select a Role and click "Add".

} -
} {!canAddSpeakers && !canAddModerator && -

{T.translate("edit_presentation.max_speakers")}

+

{T.translate("edit_presentation.max_speakers")}

} @@ -297,7 +310,7 @@ class PresentationSpeakersForm extends React.Component {
-
+
diff --git a/src/i18n/en.json b/src/i18n/en.json index ca551a6..51cbe17 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -209,7 +209,8 @@ "add_only_one_speaker": "You need to add one {speaker} to the {presentation}.", "add_exact_number_of_speakers": "You need to add {min} {speakers} to the {presentation}.", "add_min_number_speakers": "You need to add at least {min} {speakers} to the {presentation}.", - "missing_speaker": "You need to select a {speaker}." + "missing_speaker": "You need to select a {speaker}.", + "remove_speakers": "You can have at most {max} {speakers} in the {presentation}. Please remove {excess}." } }, "edit_speaker": { From 47394b971e7c60342439d5e08969e5e76e936320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Tue, 11 Aug 2026 17:57:01 -0300 Subject: [PATCH 04/10] fix: add guard on CPF, rollback i18n zh changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/presentation-speakers-form.js | 2 +- src/i18n/zh.json | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/presentation-speakers-form.js b/src/components/presentation-speakers-form.js index 695a815..8224090 100644 --- a/src/components/presentation-speakers-form.js +++ b/src/components/presentation-speakers-form.js @@ -264,7 +264,7 @@ class PresentationSpeakersForm extends React.Component { id="speaker" selectionPlanSettings={selectionPlanSettings} value={speakerInput} - speakers={entity.speakers} + speakers={Array.isArray(entity.speakers) ? entity.speakers : []} placeholder={T.translate("edit_presentation.placeholders.speakers", { speakers: `${selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || diff --git a/src/i18n/zh.json b/src/i18n/zh.json index 67f8191..3de4591 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -167,20 +167,12 @@ "review_subtitle": "您的演讲已经提交并等待专题负责人审阅", "permission_denied": "没有权限", "no_edit": "您无权编辑此演示文稿", + "add_speaker_error": "您需要添加至少一名演讲者到演讲议题中来。", "chair_comments": "Chair Comments", "presentation_material": "Presentation Material", "placeholders": { "type_id" : "-- Select One --", "level" : "-- Select One --" - }, - "errors": { - "role": "您必须选择一个角色。", - "add_moderator": "您需要添加至少一名主持人到 {presentation} 中来。", - "add_speakers": "您需要添加 {min} 到 {max} 名 {speakers} 到 {presentation} 中来。", - "add_only_one_speaker": "您需要添加一名 {speaker} 到 {presentation} 中来。", - "add_exact_number_of_speakers": "您需要添加 {min} 名 {speakers} 到 {presentation} 中来。", - "add_min_number_speakers": "您需要添加至少 {min} 名 {speakers} 到 {presentation} 中来。", - "missing_speaker": "您需要选择一个 {speaker}。" } }, "edit_speaker": { From 0c2702c6512b43b427201a966b17b89ead4d2a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 13 Aug 2026 16:07:23 -0300 Subject: [PATCH 05/10] fix: fix eventy type source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/presentation-speakers-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/presentation-speakers-form.js b/src/components/presentation-speakers-form.js index 8224090..fbaa891 100644 --- a/src/components/presentation-speakers-form.js +++ b/src/components/presentation-speakers-form.js @@ -186,7 +186,7 @@ class PresentationSpeakersForm extends React.Component { let { speakerInput, error, speaker } = this.state; let eventType = summit.event_types.find(t => t.id == entity.type_id); let speakersCount = entity.speakers?.length ?? 0; - let { max: maxSpeakers } = getSpeakerLimits(eventType); + let { max: maxSpeakers } = getSpeakerLimits(entity.type); let canAddSpeakers = (eventType && maxSpeakers > speakersCount); let canAddModerator = (eventType && eventType.max_moderators && !entity.moderator); From 6c812642cec4d72f952ed45e2212b97c5b18fcc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 13 Aug 2026 16:19:26 -0300 Subject: [PATCH 06/10] fix: split functions into speaker limits to add test cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/presentation-speakers-form.js | 37 +-------- .../__tests__/speaker-limits.test.js | 79 +++++++++++++++++++ src/components/speaker-limits/index.js | 42 ++++++++++ 3 files changed, 123 insertions(+), 35 deletions(-) create mode 100644 src/components/speaker-limits/__tests__/speaker-limits.test.js create mode 100644 src/components/speaker-limits/index.js diff --git a/src/components/presentation-speakers-form.js b/src/components/presentation-speakers-form.js index fbaa891..b5cd5d9 100644 --- a/src/components/presentation-speakers-form.js +++ b/src/components/presentation-speakers-form.js @@ -18,17 +18,7 @@ import { Exclusive, Dropdown } from 'openstack-uicore-foundation/lib/components' import T from "i18n-react/dist/i18n-react"; import CPFSpeakerInput from './inputs/speaker-input' import Swal from "sweetalert2"; - -const getSpeakerLimits = (type) => { - if (!type) return { min: 0, max: 0 }; - const defaultMin = type.are_speakers_mandatory ? 1 : 0; - const defaultMax = (type.are_speakers_mandatory || type.use_speakers) ? Infinity : 0; - const min = type.min_speakers ?? defaultMin; - const possibleMax = type.max_speakers ?? defaultMax; - // Protection against invalid configuration of max_speakers < min_speakers - const max = possibleMax >= min ? possibleMax : min; - return { min, max }; -}; +import { getSpeakerLimits, getSpeakerCountErrorField } from './speaker-limits'; class PresentationSpeakersForm extends React.Component { constructor(props) { @@ -77,30 +67,7 @@ class PresentationSpeakersForm extends React.Component { const speaker = (selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker")).toLowerCase(); const speakers = (selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || T.translate("edit_presentation.speakers")).toLowerCase(); const translationParams = { presentation, speaker, speakers, max: maxSpeakers, min: minSpeakers, excess }; - - let errorField; - if (speakersCount > maxSpeakers) { - errorField = "remove_speakers"; - } else { - switch (true) { - // There is no upper limit of speakers but there is a minimum - case (Infinity === maxSpeakers): - errorField = "add_min_number_speakers"; - break; - // There should be only one speaker - case (minSpeakers === maxSpeakers && maxSpeakers === 1): - errorField = "add_only_one_speaker"; - break; - // There should be exactly a number of speakers - case (minSpeakers === maxSpeakers && maxSpeakers !== 1): - errorField = "add_exact_number_of_speakers"; - break; - // The default error message when there is an upper limit and a minimum of speakers - default: - errorField = "add_speakers"; - break; - } - } + const errorField = getSpeakerCountErrorField(speakersCount, minSpeakers, maxSpeakers); Swal.fire("Validation error", T.translate(`edit_presentation.errors.${errorField}`, translationParams), "warning"); return; diff --git a/src/components/speaker-limits/__tests__/speaker-limits.test.js b/src/components/speaker-limits/__tests__/speaker-limits.test.js new file mode 100644 index 0000000..cb6c8cd --- /dev/null +++ b/src/components/speaker-limits/__tests__/speaker-limits.test.js @@ -0,0 +1,79 @@ +/** + * Copyright 2017 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +import { getSpeakerLimits, getSpeakerCountErrorField } from '..'; + +describe('getSpeakerLimits', () => { + it('returns zero/zero when no event type is available yet', () => { + expect(getSpeakerLimits(null)).toEqual({ min: 0, max: 0 }); + expect(getSpeakerLimits(undefined)).toEqual({ min: 0, max: 0 }); + }); + + it('defaults to at-least-one with no upper bound when speakers are mandatory and no explicit limits are configured', () => { + expect(getSpeakerLimits({ are_speakers_mandatory: true })).toEqual({ min: 1, max: Infinity }); + }); + + it('defaults to optional/unbounded when speakers are enabled but not mandatory and no explicit limits are configured', () => { + expect(getSpeakerLimits({ use_speakers: true, are_speakers_mandatory: false })).toEqual({ min: 0, max: Infinity }); + }); + + it('defaults to zero/zero when the event type does not use speakers at all', () => { + expect(getSpeakerLimits({ use_speakers: false, are_speakers_mandatory: false })).toEqual({ min: 0, max: 0 }); + }); + + it('honors an explicit min_speakers of 0 even when speakers are mandatory', () => { + // min_speakers=0 is a real, distinct configuration from "unset" - the + // nullish check must not treat 0 as missing and fall back to defaultMin=1. + expect(getSpeakerLimits({ are_speakers_mandatory: true, min_speakers: 0 })).toEqual({ min: 0, max: Infinity }); + }); + + it('honors an explicit finite range', () => { + expect(getSpeakerLimits({ use_speakers: true, min_speakers: 2, max_speakers: 5 })).toEqual({ min: 2, max: 5 }); + }); + + it('honors an explicit exact-count configuration (min equals max)', () => { + expect(getSpeakerLimits({ use_speakers: true, min_speakers: 3, max_speakers: 3 })).toEqual({ min: 3, max: 3 }); + }); + + it('clamps max_speakers up to min_speakers when the event type is misconfigured with max below min', () => { + expect(getSpeakerLimits({ use_speakers: true, min_speakers: 3, max_speakers: 1 })).toEqual({ min: 3, max: 3 }); + }); + + it('honors an explicit max_speakers of 0 when min is also 0', () => { + expect(getSpeakerLimits({ use_speakers: true, min_speakers: 0, max_speakers: 0 })).toEqual({ min: 0, max: 0 }); + }); +}); + +describe('getSpeakerCountErrorField', () => { + it('reports remove_speakers when the count exceeds the max, even for an exact-count requirement', () => { + // over-max must win over the min===max branch below - a submitter over + // the limit needs to be told to remove speakers, not to add the exact count. + expect(getSpeakerCountErrorField(5, 3, 3)).toBe('remove_speakers'); + }); + + it('reports add_min_number_speakers when there is no upper bound', () => { + expect(getSpeakerCountErrorField(0, 2, Infinity)).toBe('add_min_number_speakers'); + }); + + it('reports add_only_one_speaker when exactly one speaker is required', () => { + expect(getSpeakerCountErrorField(0, 1, 1)).toBe('add_only_one_speaker'); + }); + + it('reports add_exact_number_of_speakers when an exact count greater than one is required', () => { + expect(getSpeakerCountErrorField(1, 3, 3)).toBe('add_exact_number_of_speakers'); + }); + + it('reports add_speakers for a bounded range with distinct min and max', () => { + expect(getSpeakerCountErrorField(0, 2, 5)).toBe('add_speakers'); + }); +}); diff --git a/src/components/speaker-limits/index.js b/src/components/speaker-limits/index.js new file mode 100644 index 0000000..455f4af --- /dev/null +++ b/src/components/speaker-limits/index.js @@ -0,0 +1,42 @@ +/** + * Copyright 2017 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +export const getSpeakerLimits = (type) => { + if (!type) return { min: 0, max: 0 }; + const defaultMin = type.are_speakers_mandatory ? 1 : 0; + const defaultMax = (type.are_speakers_mandatory || type.use_speakers) ? Infinity : 0; + const min = type.min_speakers ?? defaultMin; + const possibleMax = type.max_speakers ?? defaultMax; + // Protection against invalid configuration of max_speakers < min_speakers + const max = possibleMax >= min ? possibleMax : min; + return { min, max }; +}; + +export const getSpeakerCountErrorField = (speakersCount, minSpeakers, maxSpeakers) => { + if (speakersCount > maxSpeakers) return "remove_speakers"; + + switch (true) { + // There is no upper limit of speakers but there is a minimum + case (Infinity === maxSpeakers): + return "add_min_number_speakers"; + // There should be only one speaker + case (minSpeakers === maxSpeakers && maxSpeakers === 1): + return "add_only_one_speaker"; + // There should be exactly a number of speakers + case (minSpeakers === maxSpeakers && maxSpeakers !== 1): + return "add_exact_number_of_speakers"; + // The default error message when there is an upper limit and a minimum of speakers + default: + return "add_speakers"; + } +}; From 9cd0ffc6997d04fd3e383edfab87c260ff6d7526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 13 Aug 2026 17:12:32 -0300 Subject: [PATCH 07/10] fix: remove min speakers case, validate speakers on review, remove default max and min MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/presentation-review-form.js | 16 ++++++ src/components/presentation-speakers-form.js | 13 ++--- .../__tests__/speaker-limits.test.js | 54 +++++++++++++------ src/components/speaker-limits/index.js | 33 ++++++++---- src/i18n/en.json | 1 - 5 files changed, 82 insertions(+), 35 deletions(-) diff --git a/src/components/presentation-review-form.js b/src/components/presentation-review-form.js index 213f0f9..a26dfa8 100644 --- a/src/components/presentation-review-form.js +++ b/src/components/presentation-review-form.js @@ -16,7 +16,9 @@ import 'awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css' import SubmitButtons from "./presentation-submit-buttons"; import { RawHTML } from 'openstack-uicore-foundation/lib/components' import T from "i18n-react/dist/i18n-react"; +import Swal from "sweetalert2"; import {getMarketingValue} from "./marketing-setting"; +import { validateSpeakerCount } from './speaker-limits'; class PresentationReviewForm extends React.Component { constructor(props) { @@ -42,6 +44,20 @@ class PresentationReviewForm extends React.Component { handleSubmit(ev) { ev.preventDefault(); + const { entity, selectionPlanSettings } = this.props; + const speakerValidation = validateSpeakerCount(entity); + + if (!speakerValidation.valid) { + const presentation = selectionPlanSettings?.CFP_PRESENTATIONS_SINGULAR_LABEL || T.translate("edit_presentation.presentation").toLowerCase(); + const speaker = (selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker")).toLowerCase(); + const speakers = (selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || T.translate("edit_presentation.speakers")).toLowerCase(); + const { errorField, min, max, excess } = speakerValidation; + const translationParams = { presentation, speaker, speakers, max, min, excess }; + + Swal.fire("Validation error", T.translate(`edit_presentation.errors.${errorField}`, translationParams), "warning"); + return; + } + this.props.onSubmit(this.props.entity); } diff --git a/src/components/presentation-speakers-form.js b/src/components/presentation-speakers-form.js index b5cd5d9..3f26c98 100644 --- a/src/components/presentation-speakers-form.js +++ b/src/components/presentation-speakers-form.js @@ -18,7 +18,7 @@ import { Exclusive, Dropdown } from 'openstack-uicore-foundation/lib/components' import T from "i18n-react/dist/i18n-react"; import CPFSpeakerInput from './inputs/speaker-input' import Swal from "sweetalert2"; -import { getSpeakerLimits, getSpeakerCountErrorField } from './speaker-limits'; +import { getSpeakerLimits, validateSpeakerCount } from './speaker-limits'; class PresentationSpeakersForm extends React.Component { constructor(props) { @@ -58,16 +58,13 @@ class PresentationSpeakersForm extends React.Component { return; } - const speakersCount = Array.isArray(entity.speakers) ? entity.speakers.length : 0; - const { min: minSpeakers, max: maxSpeakers } = getSpeakerLimits(entity.type); - const validSpeaker = !entity.type.use_speakers || (speakersCount <= maxSpeakers && speakersCount >= minSpeakers); - const excess = speakersCount - maxSpeakers; + const speakerValidation = validateSpeakerCount(entity); - if (!validSpeaker) { + if (!speakerValidation.valid) { const speaker = (selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker")).toLowerCase(); const speakers = (selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || T.translate("edit_presentation.speakers")).toLowerCase(); - const translationParams = { presentation, speaker, speakers, max: maxSpeakers, min: minSpeakers, excess }; - const errorField = getSpeakerCountErrorField(speakersCount, minSpeakers, maxSpeakers); + const { errorField, min, max, excess } = speakerValidation; + const translationParams = { presentation, speaker, speakers, max, min, excess }; Swal.fire("Validation error", T.translate(`edit_presentation.errors.${errorField}`, translationParams), "warning"); return; diff --git a/src/components/speaker-limits/__tests__/speaker-limits.test.js b/src/components/speaker-limits/__tests__/speaker-limits.test.js index cb6c8cd..d74fa87 100644 --- a/src/components/speaker-limits/__tests__/speaker-limits.test.js +++ b/src/components/speaker-limits/__tests__/speaker-limits.test.js @@ -11,7 +11,7 @@ * limitations under the License. **/ -import { getSpeakerLimits, getSpeakerCountErrorField } from '..'; +import { getSpeakerLimits, getSpeakerCountErrorField, validateSpeakerCount } from '..'; describe('getSpeakerLimits', () => { it('returns zero/zero when no event type is available yet', () => { @@ -19,22 +19,14 @@ describe('getSpeakerLimits', () => { expect(getSpeakerLimits(undefined)).toEqual({ min: 0, max: 0 }); }); - it('defaults to at-least-one with no upper bound when speakers are mandatory and no explicit limits are configured', () => { - expect(getSpeakerLimits({ are_speakers_mandatory: true })).toEqual({ min: 1, max: Infinity }); - }); - - it('defaults to optional/unbounded when speakers are enabled but not mandatory and no explicit limits are configured', () => { - expect(getSpeakerLimits({ use_speakers: true, are_speakers_mandatory: false })).toEqual({ min: 0, max: Infinity }); - }); - it('defaults to zero/zero when the event type does not use speakers at all', () => { - expect(getSpeakerLimits({ use_speakers: false, are_speakers_mandatory: false })).toEqual({ min: 0, max: 0 }); + expect(getSpeakerLimits({ use_speakers: false, are_speakers_mandatory: false, min_speakers: 0, max_speakers: 0 })).toEqual({ min: 0, max: 0 }); }); it('honors an explicit min_speakers of 0 even when speakers are mandatory', () => { - // min_speakers=0 is a real, distinct configuration from "unset" - the - // nullish check must not treat 0 as missing and fall back to defaultMin=1. - expect(getSpeakerLimits({ are_speakers_mandatory: true, min_speakers: 0 })).toEqual({ min: 0, max: Infinity }); + // min_speakers=0 is a real, distinct configuration from a positive minimum - + // the API always sends a concrete min_speakers, so 0 must pass through as-is. + expect(getSpeakerLimits({ are_speakers_mandatory: true, min_speakers: 0, max_speakers: 5 })).toEqual({ min: 0, max: 5 }); }); it('honors an explicit finite range', () => { @@ -61,10 +53,6 @@ describe('getSpeakerCountErrorField', () => { expect(getSpeakerCountErrorField(5, 3, 3)).toBe('remove_speakers'); }); - it('reports add_min_number_speakers when there is no upper bound', () => { - expect(getSpeakerCountErrorField(0, 2, Infinity)).toBe('add_min_number_speakers'); - }); - it('reports add_only_one_speaker when exactly one speaker is required', () => { expect(getSpeakerCountErrorField(0, 1, 1)).toBe('add_only_one_speaker'); }); @@ -77,3 +65,35 @@ describe('getSpeakerCountErrorField', () => { expect(getSpeakerCountErrorField(0, 2, 5)).toBe('add_speakers'); }); }); + +describe('validateSpeakerCount', () => { + it('is valid when the event type does not use speakers at all, regardless of count', () => { + expect(validateSpeakerCount({ type: { use_speakers: false }, speakers: [] })).toEqual({ valid: true }); + }); + + it('is invalid when exactly one speaker is required and none were added', () => { + const result = validateSpeakerCount({ type: { use_speakers: true, min_speakers: 1, max_speakers: 1 }, speakers: [] }); + expect(result.valid).toBe(false); + expect(result.errorField).toBe('add_only_one_speaker'); + }); + + it('is valid once enough speakers were added to satisfy the minimum', () => { + const speakers = [{ id: 1 }, { id: 2 }]; + expect(validateSpeakerCount({ type: { use_speakers: true, min_speakers: 2, max_speakers: 5 }, speakers })).toEqual({ valid: true }); + }); + + it('is invalid, with the correct excess, when the count exceeds the max', () => { + // regression test for the Complete-action bypass: a speaker removed on the Speakers step + // can leave a presentation over/under limits without ever re-running this check there, so + // this same function must also gate the Review step's Complete action before it submits. + const speakers = [{ id: 1 }, { id: 2 }, { id: 3 }]; + const result = validateSpeakerCount({ type: { use_speakers: true, min_speakers: 1, max_speakers: 2 }, speakers }); + expect(result).toEqual({ valid: false, errorField: 'remove_speakers', min: 1, max: 2, excess: 1 }); + }); + + it('is invalid when the entity has no speakers array at all', () => { + const result = validateSpeakerCount({ type: { use_speakers: true, min_speakers: 1, max_speakers: 1 } }); + expect(result.valid).toBe(false); + expect(result.errorField).toBe('add_only_one_speaker'); + }); +}); diff --git a/src/components/speaker-limits/index.js b/src/components/speaker-limits/index.js index 455f4af..48e6b4d 100644 --- a/src/components/speaker-limits/index.js +++ b/src/components/speaker-limits/index.js @@ -13,22 +13,17 @@ export const getSpeakerLimits = (type) => { if (!type) return { min: 0, max: 0 }; - const defaultMin = type.are_speakers_mandatory ? 1 : 0; - const defaultMax = (type.are_speakers_mandatory || type.use_speakers) ? Infinity : 0; - const min = type.min_speakers ?? defaultMin; - const possibleMax = type.max_speakers ?? defaultMax; + // min_speakers/max_speakers are non-nullable ints on the API's PresentationType + // (see PresentationTypeSerializer) - always present, never Infinity/unbounded. + const { min_speakers: min, max_speakers: max } = type; // Protection against invalid configuration of max_speakers < min_speakers - const max = possibleMax >= min ? possibleMax : min; - return { min, max }; + return { min, max: max >= min ? max : min }; }; export const getSpeakerCountErrorField = (speakersCount, minSpeakers, maxSpeakers) => { if (speakersCount > maxSpeakers) return "remove_speakers"; switch (true) { - // There is no upper limit of speakers but there is a minimum - case (Infinity === maxSpeakers): - return "add_min_number_speakers"; // There should be only one speaker case (minSpeakers === maxSpeakers && maxSpeakers === 1): return "add_only_one_speaker"; @@ -40,3 +35,23 @@ export const getSpeakerCountErrorField = (speakersCount, minSpeakers, maxSpeaker return "add_speakers"; } }; + +// Single source of truth for "is this presentation's speaker count valid" - used both +// by the Speakers step (on save) and the Review step (on final Complete), so the +// Complete action can't finalize a presentation the Speakers step would have rejected. +export const validateSpeakerCount = (entity) => { + if (!entity?.type?.use_speakers) return { valid: true }; + + const speakersCount = Array.isArray(entity.speakers) ? entity.speakers.length : 0; + const { min, max } = getSpeakerLimits(entity.type); + + if (speakersCount <= max && speakersCount >= min) return { valid: true }; + + return { + valid: false, + errorField: getSpeakerCountErrorField(speakersCount, min, max), + min, + max, + excess: speakersCount - max + }; +}; diff --git a/src/i18n/en.json b/src/i18n/en.json index 51cbe17..83438e1 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -208,7 +208,6 @@ "add_speakers": "You need to add between {min} and {max} {speakers} to the {presentation}.", "add_only_one_speaker": "You need to add one {speaker} to the {presentation}.", "add_exact_number_of_speakers": "You need to add {min} {speakers} to the {presentation}.", - "add_min_number_speakers": "You need to add at least {min} {speakers} to the {presentation}.", "missing_speaker": "You need to select a {speaker}.", "remove_speakers": "You can have at most {max} {speakers} in the {presentation}. Please remove {excess}." } From 17ad6e1b78c053cc49fbf7cebbf0188aabac3cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 20 Aug 2026 08:10:58 -0300 Subject: [PATCH 08/10] fix: unify validation, apply on presentation review, add unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/presentation-review-form.js | 15 +- src/components/presentation-speakers-form.js | 22 +-- .../presentation-submit-validation.test.js | 160 ++++++++++++++++++ .../index.js | 27 +++ .../__tests__/speaker-limits.test.js | 99 ----------- 5 files changed, 197 insertions(+), 126 deletions(-) create mode 100644 src/components/presentation-submit-validation/__tests__/presentation-submit-validation.test.js rename src/components/{speaker-limits => presentation-submit-validation}/index.js (62%) delete mode 100644 src/components/speaker-limits/__tests__/speaker-limits.test.js diff --git a/src/components/presentation-review-form.js b/src/components/presentation-review-form.js index a26dfa8..9b49770 100644 --- a/src/components/presentation-review-form.js +++ b/src/components/presentation-review-form.js @@ -18,7 +18,7 @@ import { RawHTML } from 'openstack-uicore-foundation/lib/components' import T from "i18n-react/dist/i18n-react"; import Swal from "sweetalert2"; import {getMarketingValue} from "./marketing-setting"; -import { validateSpeakerCount } from './speaker-limits'; +import { getSubmitValidationError } from './presentation-submit-validation'; class PresentationReviewForm extends React.Component { constructor(props) { @@ -45,16 +45,11 @@ class PresentationReviewForm extends React.Component { ev.preventDefault(); const { entity, selectionPlanSettings } = this.props; - const speakerValidation = validateSpeakerCount(entity); + const validationError = getSubmitValidationError(entity, selectionPlanSettings); - if (!speakerValidation.valid) { - const presentation = selectionPlanSettings?.CFP_PRESENTATIONS_SINGULAR_LABEL || T.translate("edit_presentation.presentation").toLowerCase(); - const speaker = (selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker")).toLowerCase(); - const speakers = (selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || T.translate("edit_presentation.speakers")).toLowerCase(); - const { errorField, min, max, excess } = speakerValidation; - const translationParams = { presentation, speaker, speakers, max, min, excess }; - - Swal.fire("Validation error", T.translate(`edit_presentation.errors.${errorField}`, translationParams), "warning"); + if (validationError) { + const { errorField, params } = validationError; + Swal.fire("Validation error", T.translate(`edit_presentation.errors.${errorField}`, params), "warning"); return; } diff --git a/src/components/presentation-speakers-form.js b/src/components/presentation-speakers-form.js index 3f26c98..9fad7f7 100644 --- a/src/components/presentation-speakers-form.js +++ b/src/components/presentation-speakers-form.js @@ -18,7 +18,7 @@ import { Exclusive, Dropdown } from 'openstack-uicore-foundation/lib/components' import T from "i18n-react/dist/i18n-react"; import CPFSpeakerInput from './inputs/speaker-input' import Swal from "sweetalert2"; -import { getSpeakerLimits, validateSpeakerCount } from './speaker-limits'; +import { getSpeakerLimits, getSubmitValidationError } from './presentation-submit-validation'; class PresentationSpeakersForm extends React.Component { constructor(props) { @@ -50,23 +50,11 @@ class PresentationSpeakersForm extends React.Component { ev.preventDefault(); const { selectionPlanSettings, entity } = this.props; - const validModerator = !entity.type.use_moderator || !entity.type.is_moderator_mandatory || entity.moderator; - const presentation = selectionPlanSettings?.CFP_PRESENTATIONS_SINGULAR_LABEL || T.translate("edit_presentation.presentation").toLowerCase(); + const validationError = getSubmitValidationError(entity, selectionPlanSettings); - if (!validModerator) { - Swal.fire("Validation error", T.translate("edit_presentation.errors.add_moderator", { presentation }), "warning"); - return; - } - - const speakerValidation = validateSpeakerCount(entity); - - if (!speakerValidation.valid) { - const speaker = (selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker")).toLowerCase(); - const speakers = (selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || T.translate("edit_presentation.speakers")).toLowerCase(); - const { errorField, min, max, excess } = speakerValidation; - const translationParams = { presentation, speaker, speakers, max, min, excess }; - - Swal.fire("Validation error", T.translate(`edit_presentation.errors.${errorField}`, translationParams), "warning"); + if (validationError) { + const { errorField, params } = validationError; + Swal.fire("Validation error", T.translate(`edit_presentation.errors.${errorField}`, params), "warning"); return; } diff --git a/src/components/presentation-submit-validation/__tests__/presentation-submit-validation.test.js b/src/components/presentation-submit-validation/__tests__/presentation-submit-validation.test.js new file mode 100644 index 0000000..52b54ef --- /dev/null +++ b/src/components/presentation-submit-validation/__tests__/presentation-submit-validation.test.js @@ -0,0 +1,160 @@ +/** + * Copyright 2017 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +import T from 'i18n-react/dist/i18n-react'; +import en from '../../../i18n/en.json'; +import { getSpeakerLimits, getSpeakerCountErrorField, validateSpeakerCount, getSubmitValidationError } from '..'; + +T.setTexts(en); + +describe('getSpeakerLimits', () => { + it('returns zero/zero when no event type is available yet', () => { + expect(getSpeakerLimits(null)).toEqual({ min: 0, max: 0 }); + expect(getSpeakerLimits(undefined)).toEqual({ min: 0, max: 0 }); + }); + + it.each([ + [0, 5], // an explicit 0 minimum must pass through as-is, not be coerced to a default + [3, 3] // an exact-count configuration (min equals max) + ])('honors an explicit min_speakers=%i, max_speakers=%i configuration', (min, max) => { + expect(getSpeakerLimits({ min_speakers: min, max_speakers: max })).toEqual({ min, max }); + }); + + it('clamps max_speakers up to min_speakers when the event type is misconfigured with max below min', () => { + expect(getSpeakerLimits({ min_speakers: 3, max_speakers: 1 })).toEqual({ min: 3, max: 3 }); + }); +}); + +describe('getSpeakerCountErrorField', () => { + it('reports remove_speakers when the count exceeds the max, even for an exact-count requirement', () => { + // over-max must win over the min===max branch below - a submitter over + // the limit needs to be told to remove speakers, not to add the exact count. + expect(getSpeakerCountErrorField(5, 3, 3)).toBe('remove_speakers'); + }); + + it('reports add_only_one_speaker when exactly one speaker is required', () => { + expect(getSpeakerCountErrorField(0, 1, 1)).toBe('add_only_one_speaker'); + }); + + it('reports add_exact_number_of_speakers when an exact count greater than one is required', () => { + expect(getSpeakerCountErrorField(1, 3, 3)).toBe('add_exact_number_of_speakers'); + }); + + it('reports add_speakers for a bounded range with distinct min and max', () => { + expect(getSpeakerCountErrorField(0, 2, 5)).toBe('add_speakers'); + }); +}); + +describe('validateSpeakerCount', () => { + it('is valid when the event type does not use speakers at all, regardless of count', () => { + expect(validateSpeakerCount({ type: { use_speakers: false }, speakers: [] })).toEqual({ valid: true }); + }); + + it('is invalid when exactly one speaker is required and none were added', () => { + const result = validateSpeakerCount({ type: { use_speakers: true, min_speakers: 1, max_speakers: 1 }, speakers: [] }); + expect(result.valid).toBe(false); + expect(result.errorField).toBe('add_only_one_speaker'); + }); + + it('is valid once enough speakers were added to satisfy the minimum', () => { + const speakers = [{ id: 1 }, { id: 2 }]; + expect(validateSpeakerCount({ type: { use_speakers: true, min_speakers: 2, max_speakers: 5 }, speakers })).toEqual({ valid: true }); + }); + + it('is invalid, with the correct excess, when the count exceeds the max', () => { + const speakers = [{ id: 1 }, { id: 2 }, { id: 3 }]; + const result = validateSpeakerCount({ type: { use_speakers: true, min_speakers: 1, max_speakers: 2 }, speakers }); + expect(result).toEqual({ valid: false, errorField: 'remove_speakers', min: 1, max: 2, excess: 1 }); + }); + + it('is invalid when the entity has no speakers array at all', () => { + const result = validateSpeakerCount({ type: { use_speakers: true, min_speakers: 1, max_speakers: 1 } }); + expect(result.valid).toBe(false); + expect(result.errorField).toBe('add_only_one_speaker'); + }); +}); + +describe('getSubmitValidationError', () => { + // regression coverage for the Complete-action bypass: this is the exact function + // both PresentationSpeakersForm and PresentationReviewForm call before submitting, + // so a presentation missing its mandatory moderator or its required speaker count + // must be rejected from either entry point, not just the Speakers step. + + it('blocks submission when a mandatory moderator is missing', () => { + const entity = { + type: { use_moderator: true, is_moderator_mandatory: true, use_speakers: false, min_speakers: 0, max_speakers: 0 }, + moderator: null, + speakers: [] + }; + const result = getSubmitValidationError(entity, {}); + + expect(result.errorField).toBe('add_moderator'); + expect(T.translate(`edit_presentation.errors.${result.errorField}`, result.params)) + .toBe('You need to add a moderator to the presentation.'); + }); + + it('checks the mandatory moderator before the speaker count', () => { + // a presentation missing both must still report the moderator error first - + // this is the exact ordering PresentationSpeakersForm always validated, and + // the one PresentationReviewForm's Complete action used to skip entirely. + const entity = { + type: { use_moderator: true, is_moderator_mandatory: true, use_speakers: true, min_speakers: 1, max_speakers: 1 }, + moderator: null, + speakers: [] + }; + const result = getSubmitValidationError(entity, {}); + + expect(result.errorField).toBe('add_moderator'); + }); + + it('blocks submission when the speaker count is below the minimum', () => { + const entity = { + type: { use_moderator: false, is_moderator_mandatory: false, use_speakers: true, min_speakers: 2, max_speakers: 5 }, + moderator: null, + speakers: [] + }; + const result = getSubmitValidationError(entity, {}); + + expect(result.errorField).toBe('add_speakers'); + expect(T.translate(`edit_presentation.errors.${result.errorField}`, result.params)) + .toBe('You need to add between 2 and 5 speakers to the presentation.'); + }); + + it('blocks submission when the speaker count exceeds the maximum', () => { + const entity = { + type: { use_moderator: false, is_moderator_mandatory: false, use_speakers: true, min_speakers: 1, max_speakers: 2 }, + moderator: null, + speakers: [{ id: 1 }, { id: 2 }, { id: 3 }] + }; + const result = getSubmitValidationError(entity, {}); + + expect(result.errorField).toBe('remove_speakers'); + expect(T.translate(`edit_presentation.errors.${result.errorField}`, result.params)) + .toBe('You can have at most 2 speakers in the presentation. Please remove 1.'); + }); + + it.each([ + ['the moderator and speaker-count requirements are both satisfied', { + type: { use_moderator: true, is_moderator_mandatory: true, use_speakers: true, min_speakers: 1, max_speakers: 2 }, + moderator: { id: 1 }, + speakers: [{ id: 2 }] + }], + ['the event type requires neither a moderator nor any speakers', { + type: { use_moderator: false, is_moderator_mandatory: false, use_speakers: false, min_speakers: 0, max_speakers: 0 }, + moderator: null, + speakers: [] + }] + ])('returns null when %s', (_description, entity) => { + expect(getSubmitValidationError(entity, {})).toBeNull(); + }); +}); diff --git a/src/components/speaker-limits/index.js b/src/components/presentation-submit-validation/index.js similarity index 62% rename from src/components/speaker-limits/index.js rename to src/components/presentation-submit-validation/index.js index 48e6b4d..3ffeb3f 100644 --- a/src/components/speaker-limits/index.js +++ b/src/components/presentation-submit-validation/index.js @@ -11,6 +11,8 @@ * limitations under the License. **/ +import T from "i18n-react/dist/i18n-react"; + export const getSpeakerLimits = (type) => { if (!type) return { min: 0, max: 0 }; // min_speakers/max_speakers are non-nullable ints on the API's PresentationType @@ -55,3 +57,28 @@ export const validateSpeakerCount = (entity) => { excess: speakersCount - max }; }; + +// Single source of truth for the presentation-level "can this be submitted" check - +// used by both the Speakers step (Save) and the Review step (Complete), so a +// presentation missing its mandatory moderator or its required speaker count can't +// be finalized from either entry point. The moderator requirement is checked first, +// matching the order the Speakers step originally validated in. +export const getSubmitValidationError = (entity, selectionPlanSettings) => { + const presentation = selectionPlanSettings?.CFP_PRESENTATIONS_SINGULAR_LABEL || T.translate("edit_presentation.presentation").toLowerCase(); + const validModerator = !entity.type.use_moderator || !entity.type.is_moderator_mandatory || entity.moderator; + + if (!validModerator) { + return { errorField: "add_moderator", params: { presentation } }; + } + + const speakerValidation = validateSpeakerCount(entity); + + if (!speakerValidation.valid) { + const speaker = (selectionPlanSettings?.CFP_SPEAKERS_SINGULAR_LABEL || T.translate("edit_presentation.speaker")).toLowerCase(); + const speakers = (selectionPlanSettings?.CFP_SPEAKERS_PLURAL_LABEL || T.translate("edit_presentation.speakers")).toLowerCase(); + const { errorField, min, max, excess } = speakerValidation; + return { errorField, params: { presentation, speaker, speakers, max, min, excess } }; + } + + return null; +}; diff --git a/src/components/speaker-limits/__tests__/speaker-limits.test.js b/src/components/speaker-limits/__tests__/speaker-limits.test.js deleted file mode 100644 index d74fa87..0000000 --- a/src/components/speaker-limits/__tests__/speaker-limits.test.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright 2017 OpenStack Foundation - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - **/ - -import { getSpeakerLimits, getSpeakerCountErrorField, validateSpeakerCount } from '..'; - -describe('getSpeakerLimits', () => { - it('returns zero/zero when no event type is available yet', () => { - expect(getSpeakerLimits(null)).toEqual({ min: 0, max: 0 }); - expect(getSpeakerLimits(undefined)).toEqual({ min: 0, max: 0 }); - }); - - it('defaults to zero/zero when the event type does not use speakers at all', () => { - expect(getSpeakerLimits({ use_speakers: false, are_speakers_mandatory: false, min_speakers: 0, max_speakers: 0 })).toEqual({ min: 0, max: 0 }); - }); - - it('honors an explicit min_speakers of 0 even when speakers are mandatory', () => { - // min_speakers=0 is a real, distinct configuration from a positive minimum - - // the API always sends a concrete min_speakers, so 0 must pass through as-is. - expect(getSpeakerLimits({ are_speakers_mandatory: true, min_speakers: 0, max_speakers: 5 })).toEqual({ min: 0, max: 5 }); - }); - - it('honors an explicit finite range', () => { - expect(getSpeakerLimits({ use_speakers: true, min_speakers: 2, max_speakers: 5 })).toEqual({ min: 2, max: 5 }); - }); - - it('honors an explicit exact-count configuration (min equals max)', () => { - expect(getSpeakerLimits({ use_speakers: true, min_speakers: 3, max_speakers: 3 })).toEqual({ min: 3, max: 3 }); - }); - - it('clamps max_speakers up to min_speakers when the event type is misconfigured with max below min', () => { - expect(getSpeakerLimits({ use_speakers: true, min_speakers: 3, max_speakers: 1 })).toEqual({ min: 3, max: 3 }); - }); - - it('honors an explicit max_speakers of 0 when min is also 0', () => { - expect(getSpeakerLimits({ use_speakers: true, min_speakers: 0, max_speakers: 0 })).toEqual({ min: 0, max: 0 }); - }); -}); - -describe('getSpeakerCountErrorField', () => { - it('reports remove_speakers when the count exceeds the max, even for an exact-count requirement', () => { - // over-max must win over the min===max branch below - a submitter over - // the limit needs to be told to remove speakers, not to add the exact count. - expect(getSpeakerCountErrorField(5, 3, 3)).toBe('remove_speakers'); - }); - - it('reports add_only_one_speaker when exactly one speaker is required', () => { - expect(getSpeakerCountErrorField(0, 1, 1)).toBe('add_only_one_speaker'); - }); - - it('reports add_exact_number_of_speakers when an exact count greater than one is required', () => { - expect(getSpeakerCountErrorField(1, 3, 3)).toBe('add_exact_number_of_speakers'); - }); - - it('reports add_speakers for a bounded range with distinct min and max', () => { - expect(getSpeakerCountErrorField(0, 2, 5)).toBe('add_speakers'); - }); -}); - -describe('validateSpeakerCount', () => { - it('is valid when the event type does not use speakers at all, regardless of count', () => { - expect(validateSpeakerCount({ type: { use_speakers: false }, speakers: [] })).toEqual({ valid: true }); - }); - - it('is invalid when exactly one speaker is required and none were added', () => { - const result = validateSpeakerCount({ type: { use_speakers: true, min_speakers: 1, max_speakers: 1 }, speakers: [] }); - expect(result.valid).toBe(false); - expect(result.errorField).toBe('add_only_one_speaker'); - }); - - it('is valid once enough speakers were added to satisfy the minimum', () => { - const speakers = [{ id: 1 }, { id: 2 }]; - expect(validateSpeakerCount({ type: { use_speakers: true, min_speakers: 2, max_speakers: 5 }, speakers })).toEqual({ valid: true }); - }); - - it('is invalid, with the correct excess, when the count exceeds the max', () => { - // regression test for the Complete-action bypass: a speaker removed on the Speakers step - // can leave a presentation over/under limits without ever re-running this check there, so - // this same function must also gate the Review step's Complete action before it submits. - const speakers = [{ id: 1 }, { id: 2 }, { id: 3 }]; - const result = validateSpeakerCount({ type: { use_speakers: true, min_speakers: 1, max_speakers: 2 }, speakers }); - expect(result).toEqual({ valid: false, errorField: 'remove_speakers', min: 1, max: 2, excess: 1 }); - }); - - it('is invalid when the entity has no speakers array at all', () => { - const result = validateSpeakerCount({ type: { use_speakers: true, min_speakers: 1, max_speakers: 1 } }); - expect(result.valid).toBe(false); - expect(result.errorField).toBe('add_only_one_speaker'); - }); -}); From 65e95aafa8cb3f096cdb184d2c0dde11beade0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 20 Aug 2026 08:17:00 -0300 Subject: [PATCH 09/10] fix: add guard on check for moderator in entity.type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/presentation-submit-validation/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/presentation-submit-validation/index.js b/src/components/presentation-submit-validation/index.js index 3ffeb3f..75af9a7 100644 --- a/src/components/presentation-submit-validation/index.js +++ b/src/components/presentation-submit-validation/index.js @@ -65,7 +65,7 @@ export const validateSpeakerCount = (entity) => { // matching the order the Speakers step originally validated in. export const getSubmitValidationError = (entity, selectionPlanSettings) => { const presentation = selectionPlanSettings?.CFP_PRESENTATIONS_SINGULAR_LABEL || T.translate("edit_presentation.presentation").toLowerCase(); - const validModerator = !entity.type.use_moderator || !entity.type.is_moderator_mandatory || entity.moderator; + const validModerator = !entity.type?.use_moderator || !entity.type?.is_moderator_mandatory || entity.moderator; if (!validModerator) { return { errorField: "add_moderator", params: { presentation } }; From 94321e89bf9009717d9c1e2d404bbab7112265ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 2 Sep 2026 02:11:04 -0300 Subject: [PATCH 10/10] fix: enforce minimum speakers validation only when mandatory, add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../presentation-submit-validation.test.js | 24 ++++++++++++++++--- .../presentation-submit-validation/index.js | 3 ++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/components/presentation-submit-validation/__tests__/presentation-submit-validation.test.js b/src/components/presentation-submit-validation/__tests__/presentation-submit-validation.test.js index 52b54ef..e509da3 100644 --- a/src/components/presentation-submit-validation/__tests__/presentation-submit-validation.test.js +++ b/src/components/presentation-submit-validation/__tests__/presentation-submit-validation.test.js @@ -61,11 +61,29 @@ describe('validateSpeakerCount', () => { }); it('is invalid when exactly one speaker is required and none were added', () => { - const result = validateSpeakerCount({ type: { use_speakers: true, min_speakers: 1, max_speakers: 1 }, speakers: [] }); + const result = validateSpeakerCount({ type: { use_speakers: true, are_speakers_mandatory: true, min_speakers: 1, max_speakers: 1 }, speakers: [] }); expect(result.valid).toBe(false); expect(result.errorField).toBe('add_only_one_speaker'); }); + it('does not enforce the minimum when speakers are not mandatory, mirroring fulfilSpeakersConditions()', () => { + const entity = { + type: { use_speakers: true, are_speakers_mandatory: false, min_speakers: 2, max_speakers: 5 }, + speakers: [{ id: 1 }] + }; + expect(validateSpeakerCount(entity)).toEqual({ valid: true }); + }); + + it('still enforces the maximum when speakers are not mandatory', () => { + const entity = { + type: { use_speakers: true, are_speakers_mandatory: false, min_speakers: 0, max_speakers: 2 }, + speakers: [{ id: 1 }, { id: 2 }, { id: 3 }] + }; + const result = validateSpeakerCount(entity); + expect(result.valid).toBe(false); + expect(result.errorField).toBe('remove_speakers'); + }); + it('is valid once enough speakers were added to satisfy the minimum', () => { const speakers = [{ id: 1 }, { id: 2 }]; expect(validateSpeakerCount({ type: { use_speakers: true, min_speakers: 2, max_speakers: 5 }, speakers })).toEqual({ valid: true }); @@ -78,7 +96,7 @@ describe('validateSpeakerCount', () => { }); it('is invalid when the entity has no speakers array at all', () => { - const result = validateSpeakerCount({ type: { use_speakers: true, min_speakers: 1, max_speakers: 1 } }); + const result = validateSpeakerCount({ type: { use_speakers: true, are_speakers_mandatory: true, min_speakers: 1, max_speakers: 1 } }); expect(result.valid).toBe(false); expect(result.errorField).toBe('add_only_one_speaker'); }); @@ -119,7 +137,7 @@ describe('getSubmitValidationError', () => { it('blocks submission when the speaker count is below the minimum', () => { const entity = { - type: { use_moderator: false, is_moderator_mandatory: false, use_speakers: true, min_speakers: 2, max_speakers: 5 }, + type: { use_moderator: false, is_moderator_mandatory: false, use_speakers: true, are_speakers_mandatory: true, min_speakers: 2, max_speakers: 5 }, moderator: null, speakers: [] }; diff --git a/src/components/presentation-submit-validation/index.js b/src/components/presentation-submit-validation/index.js index 75af9a7..ba3ae75 100644 --- a/src/components/presentation-submit-validation/index.js +++ b/src/components/presentation-submit-validation/index.js @@ -46,8 +46,9 @@ export const validateSpeakerCount = (entity) => { const speakersCount = Array.isArray(entity.speakers) ? entity.speakers.length : 0; const { min, max } = getSpeakerLimits(entity.type); + const minRequired = entity.type.are_speakers_mandatory ? min : 0; - if (speakersCount <= max && speakersCount >= min) return { valid: true }; + if (speakersCount <= max && speakersCount >= minRequired) return { valid: true }; return { valid: false,