diff --git a/apps/backend/db_patches/0200_ChangeFapGradeToString.sql b/apps/backend/db_patches/0200_ChangeFapGradeToString.sql new file mode 100644 index 0000000000..2ad8e272da --- /dev/null +++ b/apps/backend/db_patches/0200_ChangeFapGradeToString.sql @@ -0,0 +1,73 @@ +DO +$$ +BEGIN + IF register_patch('ChangeFapGradeToString.sql', 'TCMeldrum', 'Make grade a string for FAPs', '2025-09-04') THEN + BEGIN + + DROP VIEW review_data; + + ALTER TABLE fap_reviews + ALTER COLUMN grade type varchar(30); + + CREATE OR REPLACE VIEW public.review_data + AS SELECT proposal.proposal_pk, + proposal.proposal_id, + proposal.title, + proposal.instrument_name, + proposal.availability_time, + proposal.time_allocation, + proposal.fap_id, + proposal.rank_order, + proposal.call_id, + proposal.proposer_id, + proposal.instrument_id, + proposal.fap_time_allocation, + proposal.questionary_id, + grade.avg AS average_grade, + proposal.public_comment AS comment + FROM ( SELECT fp.proposal_pk, + p.proposal_id, + p.title, + i.name AS instrument_name, + chi.availability_time, + tr.time_allocation, + f.fap_id, + fmd.rank_order, + c.call_id, + p.proposer_id, + i.instrument_id, + fp.fap_time_allocation, + p.questionary_id, + tr.public_comment + FROM fap_proposals fp + JOIN faps f ON f.fap_id = fp.fap_id + JOIN call c ON c.call_id = fp.call_id + JOIN proposals p ON p.proposal_pk = fp.proposal_pk + JOIN technical_review tr ON tr.proposal_pk = p.proposal_pk AND tr.instrument_id = fp.instrument_id + LEFT JOIN fap_meeting_decisions fmd ON fmd.proposal_pk = p.proposal_pk + JOIN call_has_instruments chi ON chi.instrument_id = fp.instrument_id AND chi.call_id = c.call_id + JOIN instruments i ON i.instrument_id = chi.instrument_id + WHERE p.status_id <> 9 AND p.status_id <> 1) proposal + LEFT JOIN ( SELECT fr.proposal_pk, + avg(CASE + WHEN fr.grade ~ '^\d+(\.\d+)\?$' THEN fr.grade::double precision + ELSE NULL + END) AS avg + FROM fap_proposals fp + JOIN fap_reviews fr ON fr.proposal_pk = fp.proposal_pk + GROUP BY fr.proposal_pk) grade ON grade.proposal_pk = proposal.proposal_pk; + + + UPDATE questions SET default_config = jsonb_set(default_config, '{maxGrade}', '10', true) WHERE question_id = 'fap_review_basis'; + UPDATE questions SET default_config = jsonb_set(default_config, '{minGrade}', '1', true) WHERE question_id = 'fap_review_basis'; + UPDATE questions SET default_config = jsonb_set(default_config, '{decimalPoints}', '0', true) WHERE question_id = 'fap_review_basis'; + UPDATE templates_has_questions SET config = jsonb_set(config, '{maxGrade}', '10', true) WHERE question_id = 'fap_review_basis'; + UPDATE templates_has_questions SET config = jsonb_set(config, '{minGrade}', '1', true) WHERE question_id = 'fap_review_basis'; + UPDATE templates_has_questions SET config = jsonb_set(config, '{decimalPoints}', '0', true) WHERE question_id = 'fap_review_basis'; + + DELETE FROM settings WHERE settings_id = 'GRADE_PRECISION'; + END; + END IF; +END; +$$ +LANGUAGE plpgsql; \ No newline at end of file diff --git a/apps/backend/db_patches/db_seeds/0005_ProposalQuestions.sql b/apps/backend/db_patches/db_seeds/0005_ProposalQuestions.sql index 05bfd4cfb8..ac01b60896 100644 --- a/apps/backend/db_patches/db_seeds/0005_ProposalQuestions.sql +++ b/apps/backend/db_patches/db_seeds/0005_ProposalQuestions.sql @@ -389,7 +389,7 @@ INSERT INTO templates_has_questions( VALUES ( 'dynamic_multiple_choice_question', - proposal_template_id_var, proposal_topic_id_var, 6, '{"variant":"dropdown", "url":"", "jsonPath":"","isMultipleSelect":true, "apiCallRequestHeaders":[],"readPermissions":[]}' + proposal_template_id_var, proposal_topic_id_var, 10, '{"variant":"dropdown", "url":"", "jsonPath":"","isMultipleSelect":true, "apiCallRequestHeaders":[],"readPermissions":[]}' ); INSERT INTO answers( questionary_id, question_id, answer @@ -456,7 +456,7 @@ INSERT INTO templates_has_questions( VALUES ( 'instrument_picker_question', - proposal_template_id_var, proposal_topic_id_var, 6, '{"variant":"dropdown","options":[1, 2], "readPermissions":[]}' + proposal_template_id_var, proposal_topic_id_var, 11, '{"variant":"dropdown","options":[1, 2], "readPermissions":[]}' ); INSERT INTO answers( questionary_id, question_id, answer @@ -491,7 +491,7 @@ INSERT INTO templates_has_questions( VALUES ( 'technique_picker_question', - proposal_template_id_var, proposal_topic_id_var, 6, '{"variant":"dropdown","options":[1, 2], "readPermissions":[]}' + proposal_template_id_var, proposal_topic_id_var, 12, '{"variant":"dropdown","options":[1, 2], "readPermissions":[]}' ); INSERT INTO answers( questionary_id, question_id, answer diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index 7dac3e93b9..286dabc3c1 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -25,7 +25,7 @@ "@user-office-software/duo-localisation": "^1.2.0", "@user-office-software/duo-logger": "^2.3.2", "@user-office-software/duo-message-broker": "^1.6.0", - "@user-office-software/duo-validation": "^5.1.19", + "@user-office-software/duo-validation": "^5.1.20", "@user-office-software/openid": "^1.4.0", "await-to-js": "^2.1.1", "bcryptjs": "^2.4.3", @@ -3849,9 +3849,9 @@ } }, "node_modules/@user-office-software/duo-validation": { - "version": "5.1.19", - "resolved": "https://registry.npmjs.org/@user-office-software/duo-validation/-/duo-validation-5.1.19.tgz", - "integrity": "sha512-Lt5uRHrtLbJvs79A2oESebzIKn1KaW+sAKD5vlio7y5YM/13NoyTixYFTqBhuK5Gs8xDT6gaGcVf0VKESQ5mjQ==", + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@user-office-software/duo-validation/-/duo-validation-5.1.20.tgz", + "integrity": "sha512-Rm+vP2w413rj3NUV1jn8N7VYoVR0EhsWsGpoZVh1FLbGBvdOvfZ05+cY9jY56DDukBShndT9R95bPTBlfx7cFg==", "license": "ISC", "dependencies": { "luxon": "^2.5.0", diff --git a/apps/backend/package.json b/apps/backend/package.json index d5e5cfd620..06eca5abc6 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -47,7 +47,7 @@ "@user-office-software/duo-localisation": "^1.2.0", "@user-office-software/duo-logger": "^2.3.2", "@user-office-software/duo-message-broker": "^1.6.0", - "@user-office-software/duo-validation": "^5.1.19", + "@user-office-software/duo-validation": "^5.1.20", "@user-office-software/openid": "^1.4.0", "await-to-js": "^2.1.1", "bcryptjs": "^2.4.3", @@ -129,4 +129,4 @@ "npm": ">=10.9.2", "node": ">=22.0.0" } -} +} \ No newline at end of file diff --git a/apps/backend/src/config/eli/configureELIEnvironment.ts b/apps/backend/src/config/eli/configureELIEnvironment.ts index 122e162c7b..56d4ee16d6 100644 --- a/apps/backend/src/config/eli/configureELIEnvironment.ts +++ b/apps/backend/src/config/eli/configureELIEnvironment.ts @@ -102,10 +102,6 @@ async function enableDefaultELIFeatures() { settingsId: SettingsId.DEFAULT_INST_SCI_STATUS_FILTER, settingsValue: 'FEASIBILITY_REVIEW', }); - await db.updateSettings({ - settingsId: SettingsId.GRADE_PRECISION, - settingsValue: '1', - }); } async function setELIRoleNames() { diff --git a/apps/backend/src/config/ess/configureESSEnvironment.ts b/apps/backend/src/config/ess/configureESSEnvironment.ts index f3af3b462c..8e046cb9a5 100644 --- a/apps/backend/src/config/ess/configureESSEnvironment.ts +++ b/apps/backend/src/config/ess/configureESSEnvironment.ts @@ -98,10 +98,6 @@ async function enableDefaultEssFeatures() { settingsId: SettingsId.DEFAULT_INST_SCI_STATUS_FILTER, settingsValue: 'FEASIBILITY_REVIEW', }); - await db.updateSettings({ - settingsId: SettingsId.GRADE_PRECISION, - settingsValue: '1', - }); await db.updateSettings({ settingsId: SettingsId.DISPLAY_FAQ_LINK, settingsValue: 'true', diff --git a/apps/backend/src/config/stfc/configureSTFCEnvironment.ts b/apps/backend/src/config/stfc/configureSTFCEnvironment.ts index 8220f30818..21460dc795 100644 --- a/apps/backend/src/config/stfc/configureSTFCEnvironment.ts +++ b/apps/backend/src/config/stfc/configureSTFCEnvironment.ts @@ -114,10 +114,6 @@ async function enableDefaultStfcFeatures() { settingsId: SettingsId.IDLE_TIMEOUT, settingsValue: '1200000', }); - await db.updateSettings({ - settingsId: SettingsId.GRADE_PRECISION, - settingsValue: '0.01', - }); await db.updateSettings({ settingsId: SettingsId.TECH_REVIEW_OPTIONAL_WORKFLOW_STATUS, settingsValue: 'FEASIBILITY', diff --git a/apps/backend/src/datasources/mockups/FapDataSource.ts b/apps/backend/src/datasources/mockups/FapDataSource.ts index cf1475c8e4..217cfbb6c1 100644 --- a/apps/backend/src/datasources/mockups/FapDataSource.ts +++ b/apps/backend/src/datasources/mockups/FapDataSource.ts @@ -96,7 +96,7 @@ export const dummyFapReview = new Review( 1, 1, 'Dummy Fap review', - 7, + '7', 0, 1, 1, diff --git a/apps/backend/src/datasources/mockups/ReviewDataSource.ts b/apps/backend/src/datasources/mockups/ReviewDataSource.ts index e5d3084d28..c0cd722e71 100644 --- a/apps/backend/src/datasources/mockups/ReviewDataSource.ts +++ b/apps/backend/src/datasources/mockups/ReviewDataSource.ts @@ -16,7 +16,7 @@ export const dummyReview = new Review( 1, 1, 'Good proposal', - 9, + '9', 0, 1, 1, @@ -31,7 +31,7 @@ export const dummySubmittedReview = new Review( 1, 1, 'Good proposal', - 9, + '9', 1, 1, 1, @@ -62,7 +62,7 @@ export const dummyReviewBad = new Review( 1, 1, 'bad proposal', - 1, + '1', 0, 1, 1, @@ -116,7 +116,7 @@ export class ReviewDataSourceMock implements ReviewDataSource { proposalPk, userID, ' ', - 1, + '1', 1, 1, 1, @@ -133,7 +133,7 @@ export class ReviewDataSourceMock implements ReviewDataSource { 1, 1, ' ', - 1, + '1', 1, 1, 1, diff --git a/apps/backend/src/datasources/postgres/records.ts b/apps/backend/src/datasources/postgres/records.ts index 50310bd5a6..7927e0770d 100644 --- a/apps/backend/src/datasources/postgres/records.ts +++ b/apps/backend/src/datasources/postgres/records.ts @@ -293,7 +293,7 @@ export interface ReviewRecord { readonly user_id: number; readonly proposal_pk: number; readonly comment: string; - readonly grade: number; + readonly grade: string; readonly status: number; readonly fap_id: number; readonly questionary_id: number; diff --git a/apps/backend/src/models/Review.ts b/apps/backend/src/models/Review.ts index 0fdc250569..d06e827e4e 100644 --- a/apps/backend/src/models/Review.ts +++ b/apps/backend/src/models/Review.ts @@ -4,7 +4,7 @@ export class Review { public proposalPk: number, public userID: number, public comment: string, - public grade: number, + public grade: string, public status: number, public fapID: number, public questionaryID: number, diff --git a/apps/backend/src/models/Settings.ts b/apps/backend/src/models/Settings.ts index e6b92ef744..d4b8e2412a 100644 --- a/apps/backend/src/models/Settings.ts +++ b/apps/backend/src/models/Settings.ts @@ -34,7 +34,6 @@ export enum SettingsId { DEFAULT_INST_SCI_STATUS_FILTER = 'DEFAULT_INST_SCI_STATUS_FILTER', DEFAULT_INST_SCI_REVIEWER_FILTER = 'DEFAULT_INST_SCI_REVIEWER_FILTER', IDLE_TIMEOUT = 'IDLE_TIMEOUT', - GRADE_PRECISION = 'GRADE_PRECISION', USER_OFFICE_EMAIL = 'USER_OFFICE_EMAIL', SMTP_BCC_EMAIL = 'SMTP_BCC_EMAIL', TECH_REVIEW_OPTIONAL_WORKFLOW_STATUS = 'TECH_REVIEW_OPTIONAL_WORKFLOW_STATUS', diff --git a/apps/backend/src/models/questionTypes/FapReviewBasis.ts b/apps/backend/src/models/questionTypes/FapReviewBasis.ts index e67cea2b73..f3ef0fdbcd 100644 --- a/apps/backend/src/models/questionTypes/FapReviewBasis.ts +++ b/apps/backend/src/models/questionTypes/FapReviewBasis.ts @@ -5,7 +5,13 @@ import { Question } from './QuestionRegistry'; export const fapReviewBasisDefinition: Question = { dataType: DataType.FAP_REVIEW_BASIS, createBlankConfig: (): FapReviewBasisConfig => { - return new FapReviewBasisConfig(); + const config = new FapReviewBasisConfig(); + config.minGrade = 1; + config.maxGrade = 10; + config.decimalPoints = 0; + config.nonNumericOptions = []; + + return config; }, getDefaultAnswer: () => null, }; diff --git a/apps/backend/src/mutations/ReviewMutations.spec.ts b/apps/backend/src/mutations/ReviewMutations.spec.ts index c43f620578..f66ee0eb94 100644 --- a/apps/backend/src/mutations/ReviewMutations.spec.ts +++ b/apps/backend/src/mutations/ReviewMutations.spec.ts @@ -21,7 +21,7 @@ test('A reviewer can submit a review on a proposal he is on', () => { reviewMutations.updateReview(dummyUserWithRole, { reviewID: 10, comment: 'Good proposal', - grade: 9, + grade: '9', status: ReviewStatus.DRAFT, fapID: 1, questionaryID: 1, @@ -34,7 +34,7 @@ test('A user can not submit a review on a proposal', () => { reviewMutations.updateReview(dummyUserNotOnProposalWithRole, { reviewID: 1, comment: 'Good proposal', - grade: 9, + grade: '9', status: ReviewStatus.DRAFT, fapID: 1, questionaryID: 1, @@ -50,7 +50,7 @@ test('A Fap chair can not modify Fap review if it is submitted', () => { reviewMutations.updateReview(dummyFapChairWithRole, { reviewID: 5, comment: 'Good proposal test', - grade: 9, + grade: '9', status: ReviewStatus.SUBMITTED, fapID: 1, questionaryID: 1, @@ -66,7 +66,7 @@ test('A Fap secretary can not modify Fap review if it is submitted', () => { reviewMutations.updateReview(dummyFapSecretaryWithRole, { reviewID: 5, comment: 'Good proposal test', - grade: 9, + grade: '9', status: ReviewStatus.SUBMITTED, fapID: 1, questionaryID: 1, diff --git a/apps/backend/src/resolvers/mutations/UpdateReviewMutation.ts b/apps/backend/src/resolvers/mutations/UpdateReviewMutation.ts index 396462073e..13fa2f6e5c 100644 --- a/apps/backend/src/resolvers/mutations/UpdateReviewMutation.ts +++ b/apps/backend/src/resolvers/mutations/UpdateReviewMutation.ts @@ -3,7 +3,6 @@ import { ArgsType, Ctx, Field, - Float, Int, Mutation, Resolver, @@ -21,11 +20,11 @@ export class UpdateReviewArgs { @Field() public comment: string; - @Field(() => Float) - public grade: number; + @Field(() => String) + public grade: string; @Field(() => ReviewStatus) - status: ReviewStatus; + public status: ReviewStatus; @Field(() => Int) public fapID: number; diff --git a/apps/backend/src/resolvers/types/FieldConfig.ts b/apps/backend/src/resolvers/types/FieldConfig.ts index 8502c18cba..89085d7d14 100644 --- a/apps/backend/src/resolvers/types/FieldConfig.ts +++ b/apps/backend/src/resolvers/types/FieldConfig.ts @@ -58,7 +58,19 @@ export class VisitBasisConfig extends ConfigBase {} export class ExperimentSafetyReviewBasisConfig extends ConfigBase {} @ObjectType() -export class FapReviewBasisConfig extends ConfigBase {} +export class FapReviewBasisConfig extends ConfigBase { + @Field(() => [String]) + nonNumericOptions: string[]; + + @Field(() => Int) + minGrade: number; + + @Field(() => Int) + maxGrade: number; + + @Field(() => Int) + decimalPoints: number; +} @ObjectType() export class TechnicalReviewBasisConfig extends ConfigBase {} diff --git a/apps/backend/src/resolvers/types/Review.ts b/apps/backend/src/resolvers/types/Review.ts index 13aaf136fd..1ba2565022 100644 --- a/apps/backend/src/resolvers/types/Review.ts +++ b/apps/backend/src/resolvers/types/Review.ts @@ -2,7 +2,6 @@ import { Ctx, Field, FieldResolver, - Float, Int, ObjectType, Resolver, @@ -27,8 +26,8 @@ export class Review implements Partial { @Field({ nullable: true }) public comment?: string; - @Field(() => Float, { nullable: true }) - public grade?: number; + @Field(() => String, { nullable: true }) + public grade?: string; @Field(() => ReviewStatus) public status: ReviewStatus; diff --git a/apps/backend/src/utils/mathFunctions.ts b/apps/backend/src/utils/mathFunctions.ts index 95942e1e0f..e50901fb96 100644 --- a/apps/backend/src/utils/mathFunctions.ts +++ b/apps/backend/src/utils/mathFunctions.ts @@ -14,7 +14,7 @@ export const average = (numbers: number[]) => { export const getGrades = (reviews: Review[] | null | undefined) => reviews ?.filter((review) => review.status === ReviewStatus.SUBMITTED) - .map((review) => review.grade as number) ?? []; + .map((review) => parseFloat(review.grade)) ?? []; export const sortByRankOrder = ( a: FapProposalWithReviewGradesAndRanking, diff --git a/apps/e2e/cypress/e2e/FAPs.cy.ts b/apps/e2e/cypress/e2e/FAPs.cy.ts index 3a03219d61..abc851da0a 100644 --- a/apps/e2e/cypress/e2e/FAPs.cy.ts +++ b/apps/e2e/cypress/e2e/FAPs.cy.ts @@ -60,15 +60,8 @@ function readWriteReview( expect(content).to.have.string(commentContent) ); - if (settings.getEnabledSettings().get(SettingsId.GRADE_PRECISION) === '1') { - cy.get('@dialog').get('[data-cy="grade-proposal"]').click(); - - cy.get('[role="listbox"] > [role="option"]').first().click(); - - cy.get('[data-cy="grade-proposal"] input').should('have.value', '1'); - } else { - cy.get('@dialog').get('[data-cy="grade-proposal"]').click().type('1'); - } + cy.get('[data-cy="grade-proposal"]').click(); + cy.get('[data-cy="grade-proposal-options"] [role="option"]').first().click(); cy.get(`#comment_ifr`).first().focus().click(); @@ -1673,7 +1666,7 @@ context('Fap reviews tests', () => { cy.updateReview({ reviewID: proposalReviews[0].id, comment: comment1, - grade: 2, + grade: '2', status: ReviewStatus.SUBMITTED, fapID: createdFapId, questionaryID: proposalReviews[0].questionaryID, @@ -1780,16 +1773,10 @@ context('Fap reviews tests', () => { cy.setTinyMceContent('comment', faker.lorem.words(3)); - if ( - settings.getEnabledSettings().get(SettingsId.GRADE_PRECISION) === '1' - ) { - cy.get('[data-cy="grade-proposal"]').click(); - cy.get('[data-cy="grade-proposal-options"] [role="option"]') - .first() - .click(); - } else { - cy.get('[data-cy="grade-proposal"]').click().click().type('1'); - } + cy.get('[data-cy="grade-proposal"]').click(); + cy.get('[data-cy="grade-proposal-options"] [role="option"]') + .first() + .click(); cy.get(`#comment_ifr`).first().focus().click(); @@ -2372,7 +2359,7 @@ context('Fap meeting components tests', () => { reviewID: review.id, comment: comment1, // NOTE: Make first proposal with lower standard deviation. Grades are 2 and 4 - grade: index ? 2 : 4, + grade: index ? '2' : '4', status: ReviewStatus.SUBMITTED, fapID: createdFapId, questionaryID: review.questionaryID, @@ -2390,7 +2377,7 @@ context('Fap meeting components tests', () => { reviewID: review.id, comment: comment2, // NOTE: Make second proposal with higher standard deviation. Grades are 1 and 5 - grade: index ? 1 : 5, + grade: index ? '1' : '5', status: ReviewStatus.SUBMITTED, fapID: createdFapId, questionaryID: review.questionaryID, @@ -2950,7 +2937,7 @@ context('Fap meeting components tests', () => { reviewID: review.id, comment: comment1, // NOTE: Make first proposal with lower standard deviation. Grades are 2 and 4 - grade: index ? 2 : 4, + grade: index ? '2' : '4', status: ReviewStatus.SUBMITTED, fapID: createdFapId, questionaryID: review.questionaryID, @@ -2968,7 +2955,7 @@ context('Fap meeting components tests', () => { reviewID: review.id, comment: comment2, // NOTE: Make second proposal with higher standard deviation. Grades are 1 and 5 - grade: index ? 1 : 5, + grade: index ? '1' : '5', status: ReviewStatus.SUBMITTED, fapID: createdFapId, questionaryID: review.questionaryID, @@ -3275,17 +3262,10 @@ context('Fap meeting components tests', () => { const commentContent = faker.lorem.words(3); cy.setTinyMceContent('comment', commentContent); - if ( - settings.getEnabledSettings().get(SettingsId.GRADE_PRECISION) === '1' - ) { - cy.get('@dialog').get('[data-cy="grade-proposal"]').click(); - - cy.get('[role="listbox"] > [role="option"]').first().click(); - - cy.get('[data-cy="grade-proposal"] input').should('have.value', '1'); - } else { - cy.get('@dialog').get('[data-cy="grade-proposal"]').click().type('1'); - } + cy.get('[data-cy="grade-proposal"]').click(); + cy.get('[data-cy="grade-proposal-options"] [role="option"]') + .first() + .click(); cy.get(`#comment_ifr`).first().focus().click(); @@ -3542,7 +3522,7 @@ context('Fap meeting components tests', () => { reviewID: review.id, comment: comment1, // NOTE: Make first proposal with lower standard deviation. Grades are 2 and 4 - grade: index ? 2 : 4, + grade: index ? '2' : '4', status: ReviewStatus.SUBMITTED, fapID: createdFapId, questionaryID: review.questionaryID, @@ -3560,7 +3540,7 @@ context('Fap meeting components tests', () => { reviewID: review.id, comment: comment2, // NOTE: Make second proposal with higher standard deviation. Grades are 1 and 5 - grade: index ? 1 : 5, + grade: index ? '1' : '5', status: ReviewStatus.SUBMITTED, fapID: createdFapId, questionaryID: review.questionaryID, @@ -3776,7 +3756,7 @@ context('Fap meeting components tests', () => { reviewID: review.id, comment: comment1, // NOTE: Make first proposal with lower standard deviation. Grades are 2 and 4 - grade: index ? 2 : 4, + grade: index ? '2' : '4', status: ReviewStatus.SUBMITTED, fapID: createdFapId, questionaryID: review.questionaryID, @@ -3794,7 +3774,7 @@ context('Fap meeting components tests', () => { reviewID: review.id, comment: comment2, // NOTE: Make second proposal with higher standard deviation. Grades are 1 and 5 - grade: index ? 1 : 5, + grade: index ? '1' : '5', status: ReviewStatus.SUBMITTED, fapID: createdFapId, questionaryID: review.questionaryID, @@ -3881,7 +3861,7 @@ context('Fap meeting components tests', () => { cy.contains('Comment is required'); cy.setTinyMceContent('comment', faker.lorem.words(3)); cy.get('[data-cy=save-and-continue-button]').focus().click(); - //cy.notification({ variant: 'success', text: 'Updated' }); + cy.notification({ variant: 'success', text: 'Saved' }); }); it('Fap Reviewer should be able to give non integer review', () => { @@ -3889,31 +3869,16 @@ context('Fap meeting components tests', () => { cy.visit('/'); cy.finishedLoading(); - cy.get('[data-cy="officer-menu-items"]').contains('Settings').click(); - cy.get('[data-cy="officer-menu-items"]').contains('App settings').click(); + cy.get('[data-cy="officer-menu-items"]').contains('Templates').click(); + cy.get('[data-cy="officer-menu-items"]').contains('FAP Review').click(); - cy.get('[data-cy="settings-table"]') - .get('input[aria-label="Search"]') - .type('GRADE_PRECISION'); + cy.get('[aria-label="Edit"]').click(); - cy.get('[data-cy="settings-table"]') - .contains('GRADE_PRECISION') - .parent() - .find('button[aria-label="Edit"]') - .click(); + cy.contains('fap_review_basis').click(); - cy.get('[data-cy="settings-table"]') - .contains('GRADE_PRECISION') - .parent() - .find(`input[value="1"]`) - .clear() - .type('0.01'); + cy.get('[data-cy="decimal_points"]').clear().type('2'); - cy.get('[data-cy="settings-table"]') - .contains('GRADE_PRECISION') - .parent() - .find('button[aria-label="Save"]') - .click(); + cy.get('[data-cy="submit"]').click(); cy.logout(); @@ -3934,16 +3899,53 @@ context('Fap meeting components tests', () => { cy.get('[data-cy="save-and-continue-button"]').focus().click(); - cy.get('[data-cy="grade-proposal"] input').then(($input) => { - expect(($input[0] as HTMLInputElement).validationMessage).to.eq( - 'Value must be less than or equal to 10.' - ); - }); + cy.contains('The grade must be a most 2 dp'); cy.get('#grade-proposal').clear().type('1.01'); cy.get('[data-cy=save-and-continue-button]').click(); - //cy.notification({ variant: 'success', text: 'Updated' }); + cy.notification({ variant: 'success', text: 'Saved' }); + }); + + it('Fap Reviewer should be able to give non numeric review', () => { + cy.login(initialDBData.users.officer); + cy.visit('/'); + cy.finishedLoading(); + + cy.get('[data-cy="officer-menu-items"]').contains('Templates').click(); + cy.get('[data-cy="officer-menu-items"]').contains('FAP Review').click(); + + cy.get('[aria-label="Edit"]').click(); + + cy.contains('fap_review_basis').click(); + + cy.get('[data-cy="add-item-button"]').click(); + + cy.get('[aria-label="Answer"]').type('A'); + + cy.get('[aria-label="Save"]').click(); + + cy.get('[data-cy="submit"]').click(); + + cy.logout(); + + cy.login(fapMembers.reviewer); + cy.visit('/'); + cy.finishedLoading(); + + cy.get('[data-cy="grade-proposal-icon"]').click(); + + cy.setTinyMceContent('comment', faker.lorem.words(3)); + + cy.contains('Classification').click(); + cy.get('[data-cy="grade-proposal"]').click(); + cy.get('[data-cy="grade-proposal-options"]').last().click(); + + cy.get('[data-cy=save-and-continue-button]').click(); + + cy.contains('Comment is required').should('not.exist'); + //TODO submiting review in tests is failing as the comment + cy.notification({ variant: 'success', text: 'Saved' }); }); }); }); @@ -4482,7 +4484,7 @@ context('Fap meeting exports test', () => { reviewID: review.id, comment: comment1, // NOTE: Make first proposal with lower standard deviation. Grades are 2 and 4 - grade: index ? 2 : 4, + grade: index ? '2' : '4', status: ReviewStatus.SUBMITTED, fapID: createdFapId, questionaryID: review.questionaryID, @@ -4500,7 +4502,7 @@ context('Fap meeting exports test', () => { reviewID: review.id, comment: comment2, // NOTE: Make second proposal with higher standard deviation. Grades are 1 and 5 - grade: index ? 1 : 5, + grade: index ? '1' : '5', status: ReviewStatus.SUBMITTED, fapID: createdFapId, questionaryID: review.questionaryID, diff --git a/apps/e2e/cypress/e2e/settings.cy.ts b/apps/e2e/cypress/e2e/settings.cy.ts index 3fb1cd2f47..4963753432 100644 --- a/apps/e2e/cypress/e2e/settings.cy.ts +++ b/apps/e2e/cypress/e2e/settings.cy.ts @@ -1172,15 +1172,10 @@ context('Settings tests', () => { cy.setTinyMceContent('comment', faker.lorem.words(3)); cy.get(`#comment_ifr`).first().focus().click(); - if ( - settings.getEnabledSettings().get(SettingsId.GRADE_PRECISION) === '1' - ) { - cy.get('[data-cy="grade-proposal"]').click(); - - cy.get('[role="listbox"] > [role="option"]').first().click(); - } else { - cy.get('[data-cy="grade-proposal"]').click().type('1'); - } + cy.get('[data-cy="grade-proposal"]').click(); + cy.get('[data-cy="grade-proposal-options"] [role="option"]') + .first() + .click(); cy.get('[data-cy="save-and-continue-button"]').focus().click(); cy.get('[data-cy="is-grade-submitted"]').click(); diff --git a/apps/e2e/cypress/fixtures/exampleCallFapExportSTFC.json b/apps/e2e/cypress/fixtures/exampleCallFapExportSTFC.json index 959c30ca76..79e3836dac 100755 --- a/apps/e2e/cypress/fixtures/exampleCallFapExportSTFC.json +++ b/apps/e2e/cypress/fixtures/exampleCallFapExportSTFC.json @@ -14,10 +14,10 @@ "Technical Review Comment": "earum ante at accusantium crustulum", "Average score": 3, "Reviewer 1": "Nils Nilsson ", - "Reviewer 1 score": 4, + "Reviewer 1 score": "4", "Reviewer 1 review comment": "earum ante at accusantium crustulum", "Reviewer 2": " Dawson ", - "Reviewer 2 score": 2, + "Reviewer 2 score": "2", "Reviewer 2 review comment": "earum ante at accusantium crustulum", "Reviewer 3": "No reviewer", "Reviewer 3 score": "-", @@ -39,10 +39,10 @@ "Technical Review Comment": "acquiro amita ad dolore cariosus", "Average score": 3, "Reviewer 1": "Nils Nilsson ", - "Reviewer 1 score": 5, + "Reviewer 1 score": "5", "Reviewer 1 review comment": "acquiro amita ad dolore cariosus", "Reviewer 2": " Dawson ", - "Reviewer 2 score": 1, + "Reviewer 2 score": "1", "Reviewer 2 review comment": "acquiro amita ad dolore cariosus", "Reviewer 3": "No reviewer", "Reviewer 3 score": "-", diff --git a/apps/e2e/cypress/fixtures/exampleFapExportSTFC.json b/apps/e2e/cypress/fixtures/exampleFapExportSTFC.json index 61eb40b40a..a4572b0a88 100755 --- a/apps/e2e/cypress/fixtures/exampleFapExportSTFC.json +++ b/apps/e2e/cypress/fixtures/exampleFapExportSTFC.json @@ -11,10 +11,10 @@ "Technical Review Comment": "earum ante at accusantium crustulum", "Average score": 3, "Reviewer 1": "Nils Nilsson ", - "Reviewer 1 score": 4, + "Reviewer 1 score": "4", "Reviewer 1 review comment": "earum ante at accusantium crustulum", "Reviewer 2": " Dawson ", - "Reviewer 2 score": 2, + "Reviewer 2 score": "2", "Reviewer 2 review comment": "earum ante at accusantium crustulum", "Reviewer 3": "No reviewer", "Reviewer 3 score": "-", @@ -32,10 +32,10 @@ "Technical Review Comment": "acquiro amita ad dolore cariosus", "Average score": 3, "Reviewer 1": "Nils Nilsson ", - "Reviewer 1 score": 5, + "Reviewer 1 score": "5", "Reviewer 1 review comment": "acquiro amita ad dolore cariosus", "Reviewer 2": " Dawson ", - "Reviewer 2 score": 1, + "Reviewer 2 score": "1", "Reviewer 2 review comment": "acquiro amita ad dolore cariosus", "Reviewer 3": "No reviewer", "Reviewer 3 score": "-", diff --git a/apps/e2e/cypress/fixtures/template_export.json b/apps/e2e/cypress/fixtures/template_export.json index 34352ec02d..6d941a3cd5 100644 --- a/apps/e2e/cypress/fixtures/template_export.json +++ b/apps/e2e/cypress/fixtures/template_export.json @@ -1,7 +1,7 @@ { "metadata": { "version": "1.2.0", - "exportDate": "2025-08-06T13:41:10.324Z" + "exportDate": "2025-10-17T11:23:20.021Z" }, "data": { "template": { @@ -246,121 +246,6 @@ "dependencies": [], "dependenciesOperator": "AND" }, - { - "question": { - "categoryId": 1, - "id": "dynamic_multiple_choice_question", - "naturalKey": "dynamic_multiple_choice_question", - "dataType": "DYNAMIC_MULTIPLE_CHOICE", - "question": "Dynamic multiple choice question from seeds", - "config": { - "required": false, - "small_label": "", - "tooltip": "", - "variant": "dropdown", - "url": "", - "jsonPath": "", - "isMultipleSelect": true, - "apiCallRequestHeaders": [], - "readPermissions": [] - } - }, - "topicId": 8, - "sortOrder": 6, - "config": { - "required": false, - "small_label": "", - "tooltip": "", - "variant": "dropdown", - "url": "", - "jsonPath": "", - "isMultipleSelect": true, - "apiCallRequestHeaders": [], - "readPermissions": [] - }, - "dependencies": [], - "dependenciesOperator": "AND" - }, - { - "question": { - "categoryId": 1, - "id": "instrument_picker_question", - "naturalKey": "instrument_picker_question", - "dataType": "INSTRUMENT_PICKER", - "question": "Instrument Picker question from seeds", - "config": { - "small_label": "", - "required": false, - "tooltip": "", - "variant": "dropdown", - "instruments": [], - "isMultipleSelect": false, - "requestTime": false, - "readPermissions": [], - "options": [ - 1, - 2 - ] - } - }, - "topicId": 8, - "sortOrder": 6, - "config": { - "small_label": "", - "required": false, - "tooltip": "", - "variant": "dropdown", - "instruments": [], - "isMultipleSelect": false, - "requestTime": false, - "readPermissions": [], - "options": [ - 1, - 2 - ] - }, - "dependencies": [], - "dependenciesOperator": "AND" - }, - { - "question": { - "categoryId": 1, - "id": "technique_picker_question", - "naturalKey": "technique_picker_question", - "dataType": "TECHNIQUE_PICKER", - "question": "Technique Picker question from seeds", - "config": { - "small_label": "", - "required": false, - "tooltip": "", - "variant": "dropdown", - "techniques": [], - "isMultipleSelect": false, - "readPermissions": [], - "options": [ - 1, - 2 - ] - } - }, - "topicId": 8, - "sortOrder": 6, - "config": { - "small_label": "", - "required": false, - "tooltip": "", - "variant": "dropdown", - "techniques": [], - "isMultipleSelect": false, - "readPermissions": [], - "options": [ - 1, - 2 - ] - }, - "dependencies": [], - "dependenciesOperator": "AND" - }, { "question": { "categoryId": 1, @@ -539,6 +424,121 @@ }, "dependencies": [], "dependenciesOperator": "AND" + }, + { + "question": { + "categoryId": 1, + "id": "dynamic_multiple_choice_question", + "naturalKey": "dynamic_multiple_choice_question", + "dataType": "DYNAMIC_MULTIPLE_CHOICE", + "question": "Dynamic multiple choice question from seeds", + "config": { + "required": false, + "small_label": "", + "tooltip": "", + "variant": "dropdown", + "url": "", + "jsonPath": "", + "isMultipleSelect": true, + "apiCallRequestHeaders": [], + "readPermissions": [] + } + }, + "topicId": 8, + "sortOrder": 10, + "config": { + "required": false, + "small_label": "", + "tooltip": "", + "variant": "dropdown", + "url": "", + "jsonPath": "", + "isMultipleSelect": true, + "apiCallRequestHeaders": [], + "readPermissions": [] + }, + "dependencies": [], + "dependenciesOperator": "AND" + }, + { + "question": { + "categoryId": 1, + "id": "instrument_picker_question", + "naturalKey": "instrument_picker_question", + "dataType": "INSTRUMENT_PICKER", + "question": "Instrument Picker question from seeds", + "config": { + "small_label": "", + "required": false, + "tooltip": "", + "variant": "dropdown", + "instruments": [], + "isMultipleSelect": false, + "requestTime": false, + "readPermissions": [], + "options": [ + 1, + 2 + ] + } + }, + "topicId": 8, + "sortOrder": 11, + "config": { + "small_label": "", + "required": false, + "tooltip": "", + "variant": "dropdown", + "instruments": [], + "isMultipleSelect": false, + "requestTime": false, + "readPermissions": [], + "options": [ + 1, + 2 + ] + }, + "dependencies": [], + "dependenciesOperator": "AND" + }, + { + "question": { + "categoryId": 1, + "id": "technique_picker_question", + "naturalKey": "technique_picker_question", + "dataType": "TECHNIQUE_PICKER", + "question": "Technique Picker question from seeds", + "config": { + "small_label": "", + "required": false, + "tooltip": "", + "variant": "dropdown", + "techniques": [], + "isMultipleSelect": false, + "readPermissions": [], + "options": [ + 1, + 2 + ] + } + }, + "topicId": 8, + "sortOrder": 12, + "config": { + "small_label": "", + "required": false, + "tooltip": "", + "variant": "dropdown", + "techniques": [], + "isMultipleSelect": false, + "readPermissions": [], + "options": [ + 1, + 2 + ] + }, + "dependencies": [], + "dependenciesOperator": "AND" } ] } @@ -654,65 +654,6 @@ "readPermissions": [] } }, - { - "categoryId": 1, - "id": "dynamic_multiple_choice_question", - "naturalKey": "dynamic_multiple_choice_question", - "dataType": "DYNAMIC_MULTIPLE_CHOICE", - "question": "Dynamic multiple choice question from seeds", - "config": { - "required": false, - "small_label": "", - "tooltip": "", - "variant": "dropdown", - "url": "", - "jsonPath": "", - "isMultipleSelect": true, - "apiCallRequestHeaders": [], - "readPermissions": [] - } - }, - { - "categoryId": 1, - "id": "instrument_picker_question", - "naturalKey": "instrument_picker_question", - "dataType": "INSTRUMENT_PICKER", - "question": "Instrument Picker question from seeds", - "config": { - "small_label": "", - "required": false, - "tooltip": "", - "variant": "dropdown", - "instruments": [], - "isMultipleSelect": false, - "requestTime": false, - "readPermissions": [], - "options": [ - 1, - 2 - ] - } - }, - { - "categoryId": 1, - "id": "technique_picker_question", - "naturalKey": "technique_picker_question", - "dataType": "TECHNIQUE_PICKER", - "question": "Technique Picker question from seeds", - "config": { - "small_label": "", - "required": false, - "tooltip": "", - "variant": "dropdown", - "techniques": [], - "isMultipleSelect": false, - "readPermissions": [], - "options": [ - 1, - 2 - ] - } - }, { "categoryId": 1, "id": "selection_from_options_question", @@ -804,6 +745,65 @@ "required": false, "small_label": "" } + }, + { + "categoryId": 1, + "id": "dynamic_multiple_choice_question", + "naturalKey": "dynamic_multiple_choice_question", + "dataType": "DYNAMIC_MULTIPLE_CHOICE", + "question": "Dynamic multiple choice question from seeds", + "config": { + "required": false, + "small_label": "", + "tooltip": "", + "variant": "dropdown", + "url": "", + "jsonPath": "", + "isMultipleSelect": true, + "apiCallRequestHeaders": [], + "readPermissions": [] + } + }, + { + "categoryId": 1, + "id": "instrument_picker_question", + "naturalKey": "instrument_picker_question", + "dataType": "INSTRUMENT_PICKER", + "question": "Instrument Picker question from seeds", + "config": { + "small_label": "", + "required": false, + "tooltip": "", + "variant": "dropdown", + "instruments": [], + "isMultipleSelect": false, + "requestTime": false, + "readPermissions": [], + "options": [ + 1, + 2 + ] + } + }, + { + "categoryId": 1, + "id": "technique_picker_question", + "naturalKey": "technique_picker_question", + "dataType": "TECHNIQUE_PICKER", + "question": "Technique Picker question from seeds", + "config": { + "small_label": "", + "required": false, + "tooltip": "", + "variant": "dropdown", + "techniques": [], + "isMultipleSelect": false, + "readPermissions": [], + "options": [ + 1, + 2 + ] + } } ], "subTemplates": [ diff --git a/apps/e2e/cypress/support/utils.ts b/apps/e2e/cypress/support/utils.ts index b9a7da06a9..0b9af3c262 100644 --- a/apps/e2e/cypress/support/utils.ts +++ b/apps/e2e/cypress/support/utils.ts @@ -189,8 +189,18 @@ const setTinyMceContent = (tinyMceId: string, content: string) => { cy.wait(1000); cy.window().then((win) => { + // setContent does not trigger change and blur event so we need to do it manually + // However, blur event needs to be triggered after a timeout as the onChange function + // may not have finished before the blur event is triggered. const editor = getEditorById(win, tinyMceId); editor?.setContent(content); + editor?.fire('change'); + // Since editor is a cypress object we cannot just use a cy.wait here + setTimeout(() => { + editor?.fire('blur'); + }, 1000); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(1000); }); }; diff --git a/apps/frontend/src/components/fap/Proposals/FapProposalsAndAssignmentsTable.tsx b/apps/frontend/src/components/fap/Proposals/FapProposalsAndAssignmentsTable.tsx index 442764131d..f44fcc1582 100644 --- a/apps/frontend/src/components/fap/Proposals/FapProposalsAndAssignmentsTable.tsx +++ b/apps/frontend/src/components/fap/Proposals/FapProposalsAndAssignmentsTable.tsx @@ -51,7 +51,7 @@ type ProposalReview = { id: number; userID: number; comment: string | null; - grade: number | null; + grade: string | null; status: ReviewStatus; fapID: number; proposalPk?: number; diff --git a/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionFapReviewBasisForm.tsx b/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionFapReviewBasisForm.tsx index e6f6387dcf..0f07cf187e 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionFapReviewBasisForm.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionFapReviewBasisForm.tsx @@ -2,12 +2,16 @@ import { Field } from 'formik'; import React from 'react'; import * as Yup from 'yup'; +import FormikUICustomTable from 'components/common/FormikUICustomTable'; import TextField from 'components/common/FormikUITextField'; +import TitledContainer from 'components/common/TitledContainer'; import { QuestionFormProps } from 'components/questionary/QuestionaryComponentRegistry'; import { useNaturalKeySchema } from 'utils/userFieldValidationSchema'; import { QuestionFormShell } from '../QuestionFormShell'; +const columns = [{ title: 'Answer', field: 'answer' }]; + export const QuestionFapReviewBasisForm = (props: QuestionFormProps) => { const field = props.question; const naturalKeySchema = useNaturalKeySchema(field.naturalKey); @@ -42,6 +46,53 @@ export const QuestionFapReviewBasisForm = (props: QuestionFormProps) => { fullWidth inputProps={{ 'data-cy': 'question' }} /> + + + + + { + return options.map((option) => { + return { answer: option }; + }); + }, + fromTable: (rows: Record[]) => { + return rows.map((row) => row.answer); + }, + }} + fullWidth + data-cy="options" + /> + )} diff --git a/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionTemplateRelationFapReviewBasisForm.tsx b/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionTemplateRelationFapReviewBasisForm.tsx index 3afcc89169..e9d585b3dc 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionTemplateRelationFapReviewBasisForm.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionTemplateRelationFapReviewBasisForm.tsx @@ -1,18 +1,81 @@ +import { Field } from 'formik'; import React from 'react'; +import * as Yup from 'yup'; +import FormikUICustomTable from 'components/common/FormikUICustomTable'; +import TextField from 'components/common/FormikUITextField'; +import TitledContainer from 'components/common/TitledContainer'; import { QuestionTemplateRelationFormProps } from 'components/questionary/QuestionaryComponentRegistry'; import { QuestionExcerpt } from '../QuestionExcerpt'; import { QuestionTemplateRelationFormShell } from '../QuestionTemplateRelationFormShell'; +const columns = [{ title: 'Answer', field: 'answer' }]; + export const QuestionTemplateRelationFapReviewBasisForm = ( props: QuestionTemplateRelationFormProps ) => { return ( - + {() => ( <> + + + + + { + return options.map((option) => { + return { answer: option }; + }); + }, + fromTable: (rows: Record[]) => { + return rows.map((row) => row.answer); + }, + }} + fullWidth + data-cy="options" + /> + )} diff --git a/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionaryComponentFapReviewBasis.tsx b/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionaryComponentFapReviewBasis.tsx index 9308f04d8e..d8be124006 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionaryComponentFapReviewBasis.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/QuestionaryComponentFapReviewBasis.tsx @@ -3,6 +3,8 @@ import { CssBaseline, FormHelperText, InputLabel, + ToggleButton, + ToggleButtonGroup, useTheme, } from '@mui/material'; import { ErrorMessage, Field } from 'formik'; @@ -12,14 +14,14 @@ import { Editor as TinyMCEEditor } from 'tinymce'; import Select from 'components/common/FormikUISelect'; import TextField from 'components/common/FormikUITextField'; import Editor from 'components/common/TinyEditor'; +import TitledContainer from 'components/common/TitledContainer'; import { BasicComponentProps } from 'components/proposal/IBasicComponentProps'; import { createMissingContextErrorMessage, QuestionaryContext, } from 'components/questionary/QuestionaryContext'; import { ReviewContextType } from 'components/review/ReviewQuestionary'; -import { SettingsContext } from 'context/SettingsContextProvider'; -import { SettingsId } from 'generated/sdk'; +import { FapReviewBasisConfig } from 'generated/sdk'; import { SubmitActionDependencyContainer } from 'hooks/questionary/useSubmitActions'; import { FapReviewSubmissionState } from 'models/questionary/fapReview/FapReviewSubmissionState'; @@ -30,6 +32,8 @@ function QuestionaryComponentFapReviewBasis(props: BasicComponentProps) { }, } = props; + const config = props.answer.config as FapReviewBasisConfig; + const theme = useTheme(); const { state, dispatch } = useContext( QuestionaryContext @@ -44,6 +48,13 @@ function QuestionaryComponentFapReviewBasis(props: BasicComponentProps) { state?.fapReview.grade || undefined ); + const [gradeType, setGradeType] = useState<'Number' | 'Classification'>( + config.nonNumericOptions.length > 0 && + config.nonNumericOptions.find((v) => v === state?.fapReview.grade) + ? 'Classification' + : 'Number' + ); + useEffect(() => { setLocalGrade(state?.fapReview.grade || undefined); }, [state]); @@ -52,12 +63,6 @@ function QuestionaryComponentFapReviewBasis(props: BasicComponentProps) { throw new Error(createMissingContextErrorMessage()); } - const { settingsMap } = useContext(SettingsContext); - - const gradeDecimalPoints = parseFloat( - settingsMap.get(SettingsId.GRADE_PRECISION)?.settingsValue?.valueOf() ?? '1' - ); - const handleCharacterCount = (editor: TinyMCEEditor) => { const wordCount = editor.plugins.wordcount; setNumberOfChars(wordCount.body.getCharacterCount()); @@ -97,61 +102,95 @@ function QuestionaryComponentFapReviewBasis(props: BasicComponentProps) { } setLocalComment(content); }} - onBlur={() => - dispatch({ + onBlur={() => { + return dispatch({ type: 'ITEM_WITH_QUESTIONARY_MODIFIED', itemWithQuestionary: { comment: localComment }, - }) - } + }); + }} /> Characters: {numberOfChars} / {6000} - - ) => { - dispatch({ - type: 'ITEM_WITH_QUESTIONARY_MODIFIED', - itemWithQuestionary: { grade: +event.target.value }, - }); - }} - formControl={{ - fullWidth: true, - required: true, - margin: 'normal', - }} - inputProps={ - gradeDecimalPoints === 1 - ? { - id: 'grade-proposal', - } - : { - id: 'grade-proposal', - step: gradeDecimalPoints, - inputMode: 'decimal', - type: 'number', - min: '1', - max: '10', - } - } - data-cy="grade-proposal" - labelId="grade-proposal-label" - options={ - gradeDecimalPoints === 1 - ? [...Array(10)].map((e, i) => ({ - text: (i + 1).toString(), - value: i + 1, - })) - : undefined - } - /> - + + {config.nonNumericOptions.length > 0 && ( + { + setGradeType(v); + setLocalGrade(undefined); + dispatch({ + type: 'ITEM_WITH_QUESTIONARY_MODIFIED', + itemWithQuestionary: { grade: undefined }, + }); + }} + title="Select grade type" + size="small" + > + Number + Classification + + )} + + + ) => { + dispatch({ + type: 'ITEM_WITH_QUESTIONARY_MODIFIED', + itemWithQuestionary: { grade: event.target.value }, + }); + }} + formControl={{ + fullWidth: true, + required: true, + margin: 'normal', + }} + inputProps={ + gradeType === 'Classification' || config.decimalPoints === 0 + ? { + id: 'grade-proposal', + } + : { + id: 'grade-proposal', + step: Math.pow(10, -config.decimalPoints).toString(), + inputMode: 'decimal', + type: 'number', + min: '1', + max: '10', + } + } + data-cy="grade-proposal" + labelId="grade-proposal-label" + options={ + gradeType === 'Classification' + ? config.nonNumericOptions.map((option) => ({ + text: option, + value: option, + })) + : config.decimalPoints === 0 + ? [...Array(10)].map((e, i) => ({ + text: (i + 1).toString(), + value: (i + 1).toString(), + })) + : undefined + } + /> + + ); @@ -171,7 +210,7 @@ const fapReviewBasisPreSubmit = status: status, fapID: fapID, questionaryID: questionaryID, - grade: grade || 0, + grade: grade || '0', comment: comment || '', }); } diff --git a/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/createFapReviewBasisValidationSchema.ts b/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/createFapReviewBasisValidationSchema.ts index 7ce229a308..3d04eb587b 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/createFapReviewBasisValidationSchema.ts +++ b/apps/frontend/src/components/questionary/questionaryComponents/FapReviewBasis/createFapReviewBasisValidationSchema.ts @@ -2,11 +2,14 @@ import { sanitizeHtmlAndCleanText } from '@user-office-software/duo-validation/l import * as Yup from 'yup'; import { QuestionaryComponentDefinition } from 'components/questionary/QuestionaryComponentRegistry'; +import { Answer, FapReviewBasisConfig } from 'generated/sdk'; export const createFapReviewBasisValidationSchema: QuestionaryComponentDefinition['createYupValidationSchema'] = - () => { + (answer: Answer) => { const FAP_REVIEW_COMMENT_CHAR_LIMIT = 6000; + const config = answer.config as FapReviewBasisConfig; + let commentSchema = Yup.string().transform(function (value: string) { return sanitizeHtmlAndCleanText(value); }); @@ -20,9 +23,41 @@ export const createFapReviewBasisValidationSchema: QuestionaryComponentDefinitio const schema = Yup.object().shape({ comment: commentSchema, - grade: Yup.number() - .min(1, 'Lowest grade is 1') - .max(10, 'Highest grade is 10') + grade: Yup.string() + .test('min', `Lowest grade is ${config.minGrade}`, (val) => { + if (val === undefined) return false; + + const number = parseFloat(val); + + return isNaN(number) ? true : number >= config.minGrade; + }) + .test('max', `Lowest grade is ${config.maxGrade}`, (val) => { + if (val === undefined) return false; + + const number = parseFloat(val); + + return isNaN(number) ? true : number <= config.maxGrade; + }) + .test( + 'decimatePoint', + `The grade must be a most ${config.decimalPoints} dp`, + (val) => { + if (val === undefined) return false; + + const number = parseFloat(val); + + const decimalPlaces = (val.split('.')[1] || '').length; + + return isNaN(number) ? true : decimalPlaces <= config.decimalPoints; + } + ) + .test('nonNumericOptions', 'Invalid option', (val) => { + if (val === undefined) return false; + + const number = parseFloat(val); + + return isNaN(number) ? config.nonNumericOptions.includes(val) : true; + }) .required('Grade is required'), }); diff --git a/apps/frontend/src/components/review/ProposalTableReviewer.tsx b/apps/frontend/src/components/review/ProposalTableReviewer.tsx index 8b1da558e7..3e96bfd751 100644 --- a/apps/frontend/src/components/review/ProposalTableReviewer.tsx +++ b/apps/frontend/src/components/review/ProposalTableReviewer.tsx @@ -43,7 +43,7 @@ type UserWithReview = { proposalId: string; proposalPk: number; title: string; - grade: number | null; + grade: string | null; reviewId: number; comment: string | null; status: ReviewStatus; diff --git a/apps/frontend/src/components/review/ReviewQuestionary.tsx b/apps/frontend/src/components/review/ReviewQuestionary.tsx index 5c65c77810..a20940859a 100644 --- a/apps/frontend/src/components/review/ReviewQuestionary.tsx +++ b/apps/frontend/src/components/review/ReviewQuestionary.tsx @@ -37,7 +37,7 @@ export function createFapReviewStub( ): FapReviewWithQuestionary { return { id: 0, - grade: 0, + grade: '0', comment: '', fapID: 0, reviewer: reviewer, diff --git a/apps/frontend/src/components/review/ReviewSummary.tsx b/apps/frontend/src/components/review/ReviewSummary.tsx index ca3e9e9cd2..582449c157 100644 --- a/apps/frontend/src/components/review/ReviewSummary.tsx +++ b/apps/frontend/src/components/review/ReviewSummary.tsx @@ -161,7 +161,7 @@ function ReviewSummary({ confirm }: ReviewSummaryProps) { toastSuccessMessage: 'Updated', }).updateReview({ reviewID: state.fapReview.id, - grade: state.fapReview.grade || 0, + grade: state.fapReview.grade || '0', comment: state.fapReview.comment || '', status: state.fapReview.status, fapID: state.fapReview.fapID, @@ -196,7 +196,7 @@ function ReviewSummary({ confirm }: ReviewSummaryProps) { 'Your review has been submitted successfully.', }).updateReview({ reviewID: state.fapReview.id, - grade: state.fapReview.grade || 0, + grade: state.fapReview.grade || '0', comment: state.fapReview.comment || '', status: ReviewStatus.SUBMITTED, fapID: state.fapReview.fapID, diff --git a/apps/frontend/src/graphql/review/updateReview.graphql b/apps/frontend/src/graphql/review/updateReview.graphql index a67ab592ad..7f98c28353 100644 --- a/apps/frontend/src/graphql/review/updateReview.graphql +++ b/apps/frontend/src/graphql/review/updateReview.graphql @@ -1,6 +1,6 @@ mutation updateReview( $reviewID: Int! - $grade: Float! + $grade: String! $comment: String! $status: ReviewStatus! $fapID: Int! diff --git a/apps/frontend/src/graphql/template/fragment.fieldConfig.graphql b/apps/frontend/src/graphql/template/fragment.fieldConfig.graphql index d66a13d5e4..f3ad627188 100644 --- a/apps/frontend/src/graphql/template/fragment.fieldConfig.graphql +++ b/apps/frontend/src/graphql/template/fragment.fieldConfig.graphql @@ -167,6 +167,10 @@ fragment fieldConfig on FieldConfig { required tooltip readPermissions + minGrade + maxGrade + decimalPoints + nonNumericOptions } ... on TechnicalReviewBasisConfig { diff --git a/apps/frontend/src/utils/mathFunctions.ts b/apps/frontend/src/utils/mathFunctions.ts index 95b9462202..178d48e5e1 100644 --- a/apps/frontend/src/utils/mathFunctions.ts +++ b/apps/frontend/src/utils/mathFunctions.ts @@ -32,5 +32,5 @@ export const standardDeviation = (numbers: number[]) => { export const getGradesFromReviews = (reviews: Review[]) => reviews - .map((review) => review.grade) + .map((review) => (!!review.grade ? parseFloat(review.grade) : null)) .filter((grade): grade is number => typeof grade === 'number');