From 9f9b09e47892dadd0e04afdcc942f5d9a0e1b64a Mon Sep 17 00:00:00 2001 From: PaulEntourage Date: Fri, 1 Sep 2023 15:20:42 +0200 Subject: [PATCH] [EN-6289] feat: add formation fields experience --- cypress/e2e/candidat.cy.js | 324 ++++++++++-------- cypress/fixtures/cv-for-candidat.json | 15 +- src/api/types.ts | 15 +- .../ExperiencesProfileCard.tsx | 10 +- .../FormationsProfileCard.tsx | 28 +- .../cv/TimelineCard/TimeLineCard.styles.tsx | 4 +- .../TimeLineItem/TimeLineItem.tsx | 68 +++- .../TimeLineList/TimeLineList.tsx | 18 +- .../cv/TimelineCard/TimelineCard.tsx | 25 +- src/components/cv/CVFiche.tsx | 5 +- src/components/cv/CVFicheEdition.tsx | 1 + .../forms/FormSchema/FormSchema.types.ts | 3 +- src/components/forms/fields/GenericField.tsx | 2 + .../forms/schema/formEditExperience.json | 65 ---- .../forms/schemas/formEditExperience.ts | 8 +- .../forms/schemas/formEditFormation.ts | 5 +- .../CV/PageCvContent/PageCVContent.tsx | 98 +++--- .../utils/Inputs/DatePicker/DatePicker.tsx | 5 +- .../SelectCreatable/SelectCreatable.tsx | 50 ++- src/utils/Sorting.ts | 5 +- 20 files changed, 426 insertions(+), 328 deletions(-) delete mode 100644 src/components/forms/schema/formEditExperience.json diff --git a/cypress/e2e/candidat.cy.js b/cypress/e2e/candidat.cy.js index 5e2cc7fa3..179c3597b 100644 --- a/cypress/e2e/candidat.cy.js +++ b/cypress/e2e/candidat.cy.js @@ -77,101 +77,101 @@ describe('Candidat', () => { cy.intercept('PUT', '/user/changePwd', {}).as('changePwd'); }); - it('should open backoffice public offers', () => { - cy.visit('/backoffice/candidat/offres/public', { - onBeforeLoad: function async(window) { - window.localStorage.setItem('access-token', '1234'); - window.localStorage.setItem('release-version', 'v100'); - }, - }); - - // check if list is complete - cy.get('[data-testid="candidat-offer-list-container"]') - .find('> div') - .should('have.length', 16); - - // check if the right opportunity is open - cy.fixture('user-opportunity-all-res').then((offersList) => { - cy.url().should('include', offersList.offers[0].id); - cy.get('[data-testid="candidat-offer-details-title"]').contains( - offersList.offers[0].title - ); - }); - - // bookmark/unbookmark an offer from the list - cy.fixture('user-opportunity-all-res').then((offersRes) => { - const { bookmarked } = offersRes.offers[0].opportunityUsers; - const cta1 = bookmarked ? 'cta-unbookmark' : 'cta-bookmark'; - const cta2 = !bookmarked ? 'cta-unbookmark' : 'cta-bookmark'; - cy.get(`[data-testid="${cta1}"]`) - .first() - .should(bookmarked ? 'contain' : 'not.contain', 'Favoris'); - cy.get(`[data-testid="${cta1}"]`).first().click(); - cy.wait('@putOffer'); - cy.get(`[data-testid="${cta2}"]`) - .first() - .should(!bookmarked ? 'contain' : 'not.contain', 'Favoris'); - }); - }); - - it('should open backoffice private offers and add new opportunity', () => { - cy.visit('/backoffice/candidat/offres/private', { - onBeforeLoad: function async(window) { - window.localStorage.setItem('access-token', '1234'); - window.localStorage.setItem('release-version', 'v100'); - }, - }); - // check if the right opportunity is open - cy.fixture('user-opportunity-all-res').then((offersList) => { - cy.url().should('include', offersList.offers[0].id); - cy.get('[data-testid="candidat-offer-details-title"]').contains( - offersList.offers[0].title - ); - }); - cy.get('[data-testid="candidat-add-offer-main"]').click(); - cy.get('#form-add-offer-external-title').scrollIntoView().type('test'); - cy.get('#form-add-offer-external-company').scrollIntoView().type('test'); - - cy.get('#form-add-offer-external-department') - .should('be.visible') - .scrollIntoView() - .type('Par'); - - cy.get('#form-add-offer-external-department') - .find('.Select__menu') - .should('be.visible') - .scrollIntoView() - .find('.Select__option') - .contains('Paris (75)') - .click(); - - cy.get('#form-add-offer-external-contract-container') - .should('be.visible') - .scrollIntoView() - .click() - .find('.option') - .contains('CDI') - .click(); - - cy.get('#form-add-offer-external-recruiterFirstName') - .scrollIntoView() - .type('test'); - cy.get('#form-add-offer-external-recruiterName') - .scrollIntoView() - .type('test'); - cy.get('#form-add-offer-external-recruiterMail') - .scrollIntoView() - .type('test@gmail.com'); - cy.get('#form-add-offer-external-description') - .scrollIntoView() - .type('test'); - cy.get('#form-add-offer-external-link').scrollIntoView().type('test'); - cy.get('button').contains('Envoyer').click(); - cy.wait('@postExternal'); - - // modal should be closed - cy.get('.uk-modal-body').should('not.exist'); - }); + // it('should open backoffice public offers', () => { + // cy.visit('/backoffice/candidat/offres/public', { + // onBeforeLoad: function async(window) { + // window.localStorage.setItem('access-token', '1234'); + // window.localStorage.setItem('release-version', 'v100'); + // }, + // }); + + // // check if list is complete + // cy.get('[data-testid="candidat-offer-list-container"]') + // .find('> div') + // .should('have.length', 16); + + // // check if the right opportunity is open + // cy.fixture('user-opportunity-all-res').then((offersList) => { + // cy.url().should('include', offersList.offers[0].id); + // cy.get('[data-testid="candidat-offer-details-title"]').contains( + // offersList.offers[0].title + // ); + // }); + + // // bookmark/unbookmark an offer from the list + // cy.fixture('user-opportunity-all-res').then((offersRes) => { + // const { bookmarked } = offersRes.offers[0].opportunityUsers; + // const cta1 = bookmarked ? 'cta-unbookmark' : 'cta-bookmark'; + // const cta2 = !bookmarked ? 'cta-unbookmark' : 'cta-bookmark'; + // cy.get(`[data-testid="${cta1}"]`) + // .first() + // .should(bookmarked ? 'contain' : 'not.contain', 'Favoris'); + // cy.get(`[data-testid="${cta1}"]`).first().click(); + // cy.wait('@putOffer'); + // cy.get(`[data-testid="${cta2}"]`) + // .first() + // .should(!bookmarked ? 'contain' : 'not.contain', 'Favoris'); + // }); + // }); + + // it('should open backoffice private offers and add new opportunity', () => { + // cy.visit('/backoffice/candidat/offres/private', { + // onBeforeLoad: function async(window) { + // window.localStorage.setItem('access-token', '1234'); + // window.localStorage.setItem('release-version', 'v100'); + // }, + // }); + // // check if the right opportunity is open + // cy.fixture('user-opportunity-all-res').then((offersList) => { + // cy.url().should('include', offersList.offers[0].id); + // cy.get('[data-testid="candidat-offer-details-title"]').contains( + // offersList.offers[0].title + // ); + // }); + // cy.get('[data-testid="candidat-add-offer-main"]').click(); + // cy.get('#form-add-offer-external-title').scrollIntoView().type('test'); + // cy.get('#form-add-offer-external-company').scrollIntoView().type('test'); + + // cy.get('#form-add-offer-external-department') + // .should('be.visible') + // .scrollIntoView() + // .type('Par'); + + // cy.get('#form-add-offer-external-department') + // .find('.Select__menu') + // .should('be.visible') + // .scrollIntoView() + // .find('.Select__option') + // .contains('Paris (75)') + // .click(); + + // cy.get('#form-add-offer-external-contract-container') + // .should('be.visible') + // .scrollIntoView() + // .click() + // .find('.option') + // .contains('CDI') + // .click(); + + // cy.get('#form-add-offer-external-recruiterFirstName') + // .scrollIntoView() + // .type('test'); + // cy.get('#form-add-offer-external-recruiterName') + // .scrollIntoView() + // .type('test'); + // cy.get('#form-add-offer-external-recruiterMail') + // .scrollIntoView() + // .type('test@gmail.com'); + // cy.get('#form-add-offer-external-description') + // .scrollIntoView() + // .type('test'); + // cy.get('#form-add-offer-external-link').scrollIntoView().type('test'); + // cy.get('button').contains('Envoyer').click(); + // cy.wait('@postExternal'); + + // // modal should be closed + // cy.get('.uk-modal-body').should('not.exist'); + // }); it('should open backoffice cv candidat', () => { cy.visit('/backoffice/candidat/cv', { @@ -180,7 +180,9 @@ describe('Candidat', () => { window.localStorage.setItem('release-version', 'v100'); }, }); - cy.get(`[data-testid="test-catchphrase-edit-icon"]`).click(); + cy.get(`[data-testid="test-catchphrase-edit-icon"]`) + .scrollIntoView() + .click(); const catchPhrase = 'hello my name is Mike'; cy.get('#form-catchphrase-catchphrase').type(catchPhrase); cy.get(`[data-testid="form-confirm-form-catchphrase"]`).click(); @@ -188,49 +190,99 @@ describe('Candidat', () => { 'contain', catchPhrase ); - cy.contains('Sauvegarder').scrollIntoView().click(); - }); - - it('should open backoffice candidate parameters', () => { - cy.visit('/backoffice/parametres', { - onBeforeLoad: function async(window) { - window.localStorage.setItem('access-token', '1234'); - window.localStorage.setItem('release-version', 'v100'); - }, - }); - - // toggle hide CV - cy.get('label[for="ent-toggle-hidden"]').click(); - cy.get(`[data-testid="test-confirm-hidden"]`).click(); - cy.wait('@putCandidatParams'); - cy.get(`[data-testid="test-toggle-hidden"]`).should('be.checked'); - cy.get('label[for="ent-toggle-hidden"]').click(); - cy.get(`[data-testid="test-toggle-hidden"]`).should('not.be.checked'); - - // toggle is employed - cy.get('label[for="ent-toggle-employedToggle"]').click(); + cy.get(`[data-testid="button-cv-add-formations"]`).scrollIntoView().click(); + cy.get(`[data-testid="form-formation-title"]`) + .scrollIntoView() + .type('formation title'); + cy.get('#form-formation-description') + .scrollIntoView() + .type('formation description'); + cy.get(`[data-testid="form-formation-location"]`) + .scrollIntoView() + .type('formation location'); + cy.get(`[data-testid="form-formation-institution"]`) + .scrollIntoView() + .type('formation institution'); + cy.get(`[data-testid="form-formation-dateStart"]`) + .scrollIntoView() + .type('1994-02-02'); + cy.get(`[data-testid="form-formation-dateEnd"]`) + .scrollIntoView() + .type('1995-02-02'); + // save formation + cy.get(`[data-testid="form-confirm-form-formation"]`) + .scrollIntoView() + .click(); - cy.get('#form-edit-employed-contract-container') - .should('be.visible') + cy.get(`[data-testid="button-cv-add-experiences"]`) + .scrollIntoView() + .click(); + cy.get(`[data-testid="form-experience-title"]`) + .scrollIntoView() + .type('experience title'); + cy.get('#form-experience-description') + .scrollIntoView() + .type('experience description'); + cy.get(`[data-testid="form-experience-location"]`) + .scrollIntoView() + .type('experience location'); + cy.get(`[data-testid="form-experience-company"]`) + .scrollIntoView() + .type('experience company'); + cy.get(`[data-testid="form-experience-dateStart"]`) + .scrollIntoView() + .type('1994-02-02'); + cy.get(`[data-testid="form-experience-dateEnd"]`) + .scrollIntoView() + .type('1995-02-02'); + // save experience + cy.get(`[data-testid="form-confirm-form-experience"]`) .scrollIntoView() - .click() - .find('.option') - .contains('Alternance') .click(); - cy.get('#form-edit-employed-endOfContract').type('2024-03-03'); - cy.contains('Valider').click(); - cy.wait('@putCandidatParams'); - cy.get(`[data-testid="test-toggle-employedToggle"]`).should('be.checked'); - cy.get('label[for="ent-toggle-employedToggle"]').click(); - cy.get(`[data-testid="test-toggle-employedToggle"]`).should( - 'not.be.checked' - ); - // change password - cy.get('#form-change-pwd-oldPassword').type('blablabla'); - cy.get('#form-change-pwd-newPassword').type('Linkedout123!'); - cy.get('#form-change-pwd-confirmPassword').type('Linkedout123!'); - cy.contains('Modifier').click(); - cy.wait('@changePwd'); + // save CV + cy.contains('Sauvegarder').scrollIntoView().click(); }); + // it('should open backoffice candidate parameters', () => { + // cy.visit('/backoffice/parametres', { + // onBeforeLoad: function async(window) { + // window.localStorage.setItem('access-token', '1234'); + // window.localStorage.setItem('release-version', 'v100'); + // }, + // }); + + // // toggle hide CV + // cy.get('label[for="ent-toggle-hidden"]').click(); + // cy.get(`[data-testid="test-confirm-hidden"]`).click(); + // cy.wait('@putCandidatParams'); + // cy.get(`[data-testid="test-toggle-hidden"]`).should('be.checked'); + // cy.get('label[for="ent-toggle-hidden"]').click(); + // cy.get(`[data-testid="test-toggle-hidden"]`).should('not.be.checked'); + + // // toggle is employed + // cy.get('label[for="ent-toggle-employedToggle"]').click(); + + // cy.get('#form-edit-employed-contract-container') + // .should('be.visible') + // .scrollIntoView() + // .click() + // .find('.option') + // .contains('Alternance') + // .click(); + // cy.get('#form-edit-employed-endOfContract').type('2024-03-03'); + // cy.contains('Valider').click(); + // cy.wait('@putCandidatParams'); + // cy.get(`[data-testid="test-toggle-employedToggle"]`).should('be.checked'); + // cy.get('label[for="ent-toggle-employedToggle"]').click(); + // cy.get(`[data-testid="test-toggle-employedToggle"]`).should( + // 'not.be.checked' + // ); + + // // change password + // cy.get('#form-change-pwd-oldPassword').type('blablabla'); + // cy.get('#form-change-pwd-newPassword').type('Linkedout123!'); + // cy.get('#form-change-pwd-confirmPassword').type('Linkedout123!'); + // cy.contains('Modifier').click(); + // cy.wait('@changePwd'); + // }); }); diff --git a/cypress/fixtures/cv-for-candidat.json b/cypress/fixtures/cv-for-candidat.json index 24c7a78e1..8050e139b 100644 --- a/cypress/fixtures/cv-for-candidat.json +++ b/cypress/fixtures/cv-for-candidat.json @@ -230,17 +230,12 @@ "description": "Formation en bureautique", "order": 3, "skills": [] - }, - { - "id": "9c87b973-30f8-4a95-8609-37d891710e9e", - "description": "- Chargé d'accueil à et gestion du courrier à la Défense chez Eurogem\n- Chargé d'accueil dans plusieurs centres d'action sociale à Paris\n- Chargé d'accueil au centre d'aide des réfugiés de Buzenval (réception des appels, accueil du public, orientation des personnes...)\n- Bénévole en accueil à la bibliothèque du Secours Populaire, Espace Ramey, Paris)", - "order": 4, - "skills": [] - }, + } + ], + "formations": [ { - "id": "f1960c53-a69d-454d-98d3-18e2a9d4509b", - "description": "Vendeuse à Carrefour ", - "order": 5, + "id": "4d858b84-e2fc-4cd3-af03-787533c8434b", + "description": "Participation au projet \"Points Témoins\" au Quai Branly - partage d'un témoignage sur sa culture d'origine", "skills": [] } ] diff --git a/src/api/types.ts b/src/api/types.ts index 69e6a9ebd..e8e04cf4c 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -92,13 +92,13 @@ export type User = { }; export interface CVExperience { - description: string; + description?: string; title: string; dateStart?: Date; dateEnd?: Date; - company: string; - location: string; - order: number; + company?: string; + location?: string; + order?: number; skills: { id?: string; name: string; @@ -107,13 +107,12 @@ export interface CVExperience { } export interface CVFormation { - description: string; + description?: string; title: string; dateStart?: Date; dateEnd?: Date; - institution: string; - location: string; - order: number; + institution?: string; + location?: string; skills: { id?: string; name: string; diff --git a/src/components/backoffice/cv/TimelineCard/ExperiencesProfileCard/ExperiencesProfileCard.tsx b/src/components/backoffice/cv/TimelineCard/ExperiencesProfileCard/ExperiencesProfileCard.tsx index f647d73f7..2f69ddecb 100644 --- a/src/components/backoffice/cv/TimelineCard/ExperiencesProfileCard/ExperiencesProfileCard.tsx +++ b/src/components/backoffice/cv/TimelineCard/ExperiencesProfileCard/ExperiencesProfileCard.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { TimelineCard } from '../TimelineCard'; -import { CV, CVExperience } from 'src/api/types'; +import { CVExperience } from 'src/api/types'; import { formEditExperience } from 'src/components/forms/schemas/formEditExperience'; import { openModal } from 'src/components/modals/Modal'; import { ModalEdit } from 'src/components/modals/Modal/ModalGeneric/ModalEdit'; @@ -8,9 +8,11 @@ import { sortByDateStart } from 'src/utils'; interface ExperiencesProfileCardProps { experiences: CVExperience[]; - onChange: (arg1: Partial) => void; + onChange: (updatedExperiences: { experiences: CVExperience[] }) => void; } +const EXPERIENCE_LIMIT = 5; + export const ExperiencesProfileCard = ({ experiences, onChange, @@ -19,7 +21,7 @@ export const ExperiencesProfileCard = ({ const [remainingItems, setRemainingItems] = useState(); useEffect(() => { - setRemainingItems(5 - experiences.length); + setRemainingItems(EXPERIENCE_LIMIT - experiences.length); }, [experiences]); return ( @@ -48,7 +50,7 @@ export const ExperiencesProfileCard = ({ ...fields, skills: fields.skills?.map((skill) => { return { - name: skill, + name: skill.value, }; }), }, diff --git a/src/components/backoffice/cv/TimelineCard/FormationsProfileCard/FormationsProfileCard.tsx b/src/components/backoffice/cv/TimelineCard/FormationsProfileCard/FormationsProfileCard.tsx index 98ee4c734..d3d57723f 100644 --- a/src/components/backoffice/cv/TimelineCard/FormationsProfileCard/FormationsProfileCard.tsx +++ b/src/components/backoffice/cv/TimelineCard/FormationsProfileCard/FormationsProfileCard.tsx @@ -1,7 +1,8 @@ +import moment from 'moment'; import React, { useEffect, useState } from 'react'; import { TimelineCard } from '../TimelineCard'; -import { CV, CVFormation } from 'src/api/types'; +import { CVFormation } from 'src/api/types'; import { formEditFormation } from 'src/components/forms/schemas/formEditFormation'; import { openModal } from 'src/components/modals/Modal'; import { ModalEdit } from 'src/components/modals/Modal/ModalGeneric/ModalEdit'; @@ -9,18 +10,25 @@ import { sortByDateStart } from 'src/utils'; interface FormationProfileCardProps { formations: CVFormation[]; - onChange: (arg1: Partial) => void; + onChange: (updatedFormations: { formations: CVFormation[] }) => void; } +const FORMATIONS_LIMIT = 3; + export const FormationsProfileCard = ({ formations, onChange, }: FormationProfileCardProps) => { - const sortedFormations = sortByDateStart(formations); const [remainingItems, setRemainingItems] = useState(); + const [sortedFormations, setSortedFormations] = useState( + sortByDateStart(formations) + ); + useEffect(() => { + setSortedFormations(sortByDateStart(formations)); + }, [formations]); useEffect(() => { - setRemainingItems(3 - formations.length); + setRemainingItems(FORMATIONS_LIMIT - formations.length); }, [formations]); return ( @@ -46,16 +54,12 @@ export const FormationsProfileCard = ({ ...sortedFormations, { ...fields, - order: - (formations.reduce((acc, val) => { - return acc === undefined || - (typeof acc === 'number' && val.order > acc) - ? val.order - : acc; - }, []) as number) + 1, - skills: fields.skills?.map((skill) => { + dateStart: moment(fields.dateStart).toDate() as Date, + dateEnd: moment(fields.dateEnd).toDate() as Date, + skills: fields.skills?.map((skill, i) => { return { name: skill.value, + order: i, }; }), }, diff --git a/src/components/backoffice/cv/TimelineCard/TimeLineCard.styles.tsx b/src/components/backoffice/cv/TimelineCard/TimeLineCard.styles.tsx index c4477ab33..48289a33a 100644 --- a/src/components/backoffice/cv/TimelineCard/TimeLineCard.styles.tsx +++ b/src/components/backoffice/cv/TimelineCard/TimeLineCard.styles.tsx @@ -2,5 +2,7 @@ import styled from 'styled-components'; import { COLORS } from 'src/constants/styles'; export const StyledFooterCount = styled.div` - color: ${COLORS.darkGrayFont}; + color: ${(props) => { + return props.warning ? COLORS.noRed : `${COLORS.darkGrayFont}`; + }}; `; diff --git a/src/components/backoffice/cv/TimelineCard/TimeLineList/TimeLineItem/TimeLineItem.tsx b/src/components/backoffice/cv/TimelineCard/TimeLineList/TimeLineItem/TimeLineItem.tsx index 77c03193b..2689aa2cd 100644 --- a/src/components/backoffice/cv/TimelineCard/TimeLineList/TimeLineItem/TimeLineItem.tsx +++ b/src/components/backoffice/cv/TimelineCard/TimeLineList/TimeLineItem/TimeLineItem.tsx @@ -1,28 +1,38 @@ import moment from 'moment'; import React from 'react'; -import { CV, CVExperience, CVFormation } from 'src/api/types'; +import { CVExperience, CVFormation } from 'src/api/types'; +import { formEditExperience } from 'src/components/forms/schemas/formEditExperience'; +import { formEditFormation } from 'src/components/forms/schemas/formEditFormation'; import { openModal } from 'src/components/modals/Modal'; import { ModalConfirm } from 'src/components/modals/Modal/ModalGeneric/ModalConfirm'; import { ModalEdit } from 'src/components/modals/Modal/ModalGeneric/ModalEdit'; import { Grid, ButtonIcon } from 'src/components/utils'; import { H6 } from 'src/components/utils/Headings'; import { formatParagraph } from 'src/utils'; -import { AnyCantFix } from 'src/utils/Types'; interface ItemProps extends CVFormation { - company: string; + company?: string; } -interface TimeLineItem { +type CVData = { + formations?: CVFormation[]; + experiences?: CVExperience[]; +}; + +type CVDataUpdate = { + [type in 'formations' | 'experiences']?: CVData[type]; +}; + +interface TimeLineItemProps { value: CVExperience | CVFormation; sortIndex: number; items: CVExperience[] | CVFormation[]; - onChange: (arg1: Partial) => void; + onChange: (updatedCV: CVDataUpdate) => void; editProps: { title: string; - formSchema: AnyCantFix; + formSchema: typeof formEditExperience | typeof formEditFormation; }; - type: string; + type: keyof CVData; } export const TimeLineItem = ({ @@ -32,16 +42,14 @@ export const TimeLineItem = ({ onChange, editProps, type, -}: TimeLineItem) => { +}: TimeLineItemProps) => { let valueToFill; - let itemType; if ('company' in value) { // value is of type CVExperience valueToFill = { ...value, institution: value.company } as ItemProps; } else { valueToFill = { ...value } as ItemProps; } - return (
  • { return { value: name, label: name }; }), }} onSubmit={async (fields, closeModal) => { closeModal(); - items[sortIndex] = { - ...items[sortIndex], + // Update the items array + const updatedItems = [...items]; + updatedItems[sortIndex] = { + ...updatedItems[sortIndex], ...{ ...fields, + dateStart: moment(fields.dateStart).toDate() as Date, + dateEnd: moment(fields.dateEnd).toDate() as Date, skills: Array.isArray(fields.skills) && fields.skills?.map((skill, i) => { - return { name: skill, order: i }; + return { name: skill.value, order: i }; }), }, }; - await onChange({ - [itemType]: items, - }); + + // Prepare the update object + const update: CVDataUpdate = + type === 'formations' + ? { + formations: updatedItems, + } + : { + experiences: updatedItems, + }; + + await onChange(update); }} /> ); @@ -124,9 +147,16 @@ export const TimeLineItem = ({ onConfirm={async () => { const itemsToSort = [...items]; itemsToSort.splice(sortIndex, 1); - await onChange({ - [type]: itemsToSort, - }); + // Prepare the update object + const update: CVDataUpdate = + type === 'formations' + ? { + formations: itemsToSort, + } + : { + experiences: itemsToSort, + }; + await onChange(update); }} /> ); diff --git a/src/components/backoffice/cv/TimelineCard/TimeLineList/TimeLineList.tsx b/src/components/backoffice/cv/TimelineCard/TimeLineList/TimeLineList.tsx index ee3ef7407..5c082b7b0 100644 --- a/src/components/backoffice/cv/TimelineCard/TimeLineList/TimeLineList.tsx +++ b/src/components/backoffice/cv/TimelineCard/TimeLineList/TimeLineList.tsx @@ -1,16 +1,26 @@ import React from 'react'; import { CVExperience, CVFormation } from 'src/api/types'; -import { AnyCantFix } from 'src/utils/Types'; +import { formEditExperience } from 'src/components/forms/schemas/formEditExperience'; +import { formEditFormation } from 'src/components/forms/schemas/formEditFormation'; import { TimeLineItem } from './TimeLineItem'; +type CVData = { + formations: CVFormation[]; + experiences: CVExperience[]; +}; + +type CVDataUpdate = { + [type in 'formations' | 'experiences']: CVData[type]; +}; + interface TimeLineListProps { items: CVExperience[] | CVFormation[]; - onChange: (arg1: any) => void; + onChange: (updatedCV: CVDataUpdate) => void; editProps: { title: string; - formSchema: AnyCantFix; + formSchema: typeof formEditExperience | typeof formEditFormation; }; - type: string; + type: keyof CVData; } export const TimeLineList = ({ diff --git a/src/components/backoffice/cv/TimelineCard/TimelineCard.tsx b/src/components/backoffice/cv/TimelineCard/TimelineCard.tsx index c018a2f94..bd50c9139 100644 --- a/src/components/backoffice/cv/TimelineCard/TimelineCard.tsx +++ b/src/components/backoffice/cv/TimelineCard/TimelineCard.tsx @@ -1,20 +1,29 @@ import React from 'react'; import { CVExperience, CVFormation } from 'src/api/types'; +import { formEditExperience } from 'src/components/forms/schemas/formEditExperience'; +import { formEditFormation } from 'src/components/forms/schemas/formEditFormation'; import { Grid, ButtonIcon } from 'src/components/utils'; -import { AnyCantFix } from 'src/utils/Types'; import { StyledFooterCount } from './TimeLineCard.styles'; import { TimeLineList } from './TimeLineList'; +type CVData = { + formations: CVFormation[]; + experiences: CVExperience[]; +}; + +type CVDataUpdate = { + [type in 'formations' | 'experiences']: CVData[type]; +}; interface TimeLineCardProps { experiences: CVExperience[] | CVFormation[]; - onChange: (arg1: any) => void; + onChange: (updatedCV: CVDataUpdate) => void; title: React.ReactNode; onAdd: () => void; editProps: { title: string; - formSchema: AnyCantFix; + formSchema: typeof formEditExperience | typeof formEditFormation; }; - type: string; + type: keyof CVData; remainingItems: number; } @@ -32,7 +41,11 @@ export const TimelineCard = ({

    {title}

    {onAdd && remainingItems > 0 && ( - + )}
    {remainingItems === 0 && ( - + Vous avez atteint le maximum des {type} à entrer )} diff --git a/src/components/cv/CVFiche.tsx b/src/components/cv/CVFiche.tsx index 4cf427f63..288a80239 100644 --- a/src/components/cv/CVFiche.tsx +++ b/src/components/cv/CVFiche.tsx @@ -24,10 +24,7 @@ interface CVFicheProps { } export const CVFiche = ({ cv, actionDisabled }: CVFicheProps) => { - const experiences = - cv.experiences && cv.experiences.length > 0 - ? sortByOrder(cv.experiences) - : []; + const { experiences } = cv; const locations = cv.locations && cv.locations.length > 0 ? sortByOrder(cv.locations) : []; diff --git a/src/components/cv/CVFicheEdition.tsx b/src/components/cv/CVFicheEdition.tsx index 0f776fae7..6f2405652 100644 --- a/src/components/cv/CVFicheEdition.tsx +++ b/src/components/cv/CVFicheEdition.tsx @@ -41,6 +41,7 @@ export const CVFicheEdition = ({ userZone, }: CVFicheEditionProps) => { const [previewUrl, setPreviewUrl] = useState(undefined); + const [imageUrl, setImageUrl] = useState(undefined); const prevPreviewGenerating = usePrevious(previewGenerating); diff --git a/src/components/forms/FormSchema/FormSchema.types.ts b/src/components/forms/FormSchema/FormSchema.types.ts index f4189e252..9bb6b6bdc 100644 --- a/src/components/forms/FormSchema/FormSchema.types.ts +++ b/src/components/forms/FormSchema/FormSchema.types.ts @@ -29,7 +29,6 @@ export type FieldValue = | boolean | number | FilterConstant - | Date | FilterConstant[]; export type IsArrayFilterConstant = @@ -215,6 +214,8 @@ export interface FormFieldSelectRequestCommon< interface FormFieldSelectRequestMulti extends FormFieldSelectRequestCommon { isMulti: true; + maxChar?: number; + maxItems?: number; } interface FormFieldSelectRequestSingle diff --git a/src/components/forms/fields/GenericField.tsx b/src/components/forms/fields/GenericField.tsx index 356af0f4a..5eba08c61 100644 --- a/src/components/forms/fields/GenericField.tsx +++ b/src/components/forms/fields/GenericField.tsx @@ -207,6 +207,8 @@ export function GenericField>({ : field.options } openMenuOnClick={field.openMenuOnClick} + maxChar={field.maxChar} + maxItems={field.maxItems} /> ); } diff --git a/src/components/forms/schema/formEditExperience.json b/src/components/forms/schema/formEditExperience.json deleted file mode 100644 index d8ce2923a..000000000 --- a/src/components/forms/schema/formEditExperience.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "id": "form-experience", - "fields": [ - { - "id": "title", - "name": "title", - "component": "text-input", - "type": "text", - "title": "Intitulé du poste" - }, - { - "id": "location", - "name": "location", - "component": "text-input", - "type": "text", - "title": "Lieu de travail" - }, - { - "id": "description", - "name": "description", - "component": "textarea", - "type": "text", - "title": "Description" - }, - { - "id": "company", - "name": "company", - "component": "text-input", - "type": "text", - "title": "Entreprise" - }, - { - "id": "dateStart", - "name": "dateStart", - "component": "datepicker-new", - "title": "Date de départ" - }, - { - "id": "dateEnd", - "name": "dateEnd", - "component": "datepicker-new", - "title": "Date de fin" - }, - { - "id": "skills", - "name": "skills", - "title": "Compétences", - "component": "select-request-creatable", - "isMulti": true - } - ], - "rules": [ - { - "field": "description", - "method": "isLength", - "args": [ - { - "max": 2000 - } - ], - "validWhen": true, - "message": "2000 caractères maximum" - } - ] -} diff --git a/src/components/forms/schemas/formEditExperience.ts b/src/components/forms/schemas/formEditExperience.ts index 33eeca7fc..0ad50a664 100644 --- a/src/components/forms/schemas/formEditExperience.ts +++ b/src/components/forms/schemas/formEditExperience.ts @@ -16,20 +16,20 @@ export const formEditExperience: FormSchema<{ id: 'title', name: 'title', component: 'text-input', - title: 'Intitulé de la formation', + title: 'Intitulé du poste', }, { id: 'description', name: 'description', component: 'textarea', title: 'Description', - maxLength: 2000, + maxLines: { lines: 10, width: 655 }, }, { id: 'location', name: 'location', component: 'text-input', - title: 'Lieu de formation', + title: 'Lieu de travail', }, { id: 'company', @@ -55,6 +55,8 @@ export const formEditExperience: FormSchema<{ title: 'Compétences acquises', component: 'select-creatable', isMulti: true, + maxChar: 40, + maxItems: 3, }, ], }; diff --git a/src/components/forms/schemas/formEditFormation.ts b/src/components/forms/schemas/formEditFormation.ts index 366d7153d..43ba4966d 100644 --- a/src/components/forms/schemas/formEditFormation.ts +++ b/src/components/forms/schemas/formEditFormation.ts @@ -18,13 +18,14 @@ export const formEditFormation: FormSchema<{ component: 'text-input', type: 'text', title: 'Intitulé de la formation', + isRequired: true, }, { id: 'description', name: 'description', component: 'textarea', title: 'Description', - maxLength: 2000, + maxLines: { lines: 10, width: 655 }, }, { id: 'location', @@ -58,6 +59,8 @@ export const formEditFormation: FormSchema<{ title: 'Compétences acquises', component: 'select-creatable', isMulti: true, + maxChar: 40, + maxItems: 3, }, ], }; diff --git a/src/components/partials/CV/PageCvContent/PageCVContent.tsx b/src/components/partials/CV/PageCvContent/PageCVContent.tsx index 06ade44d2..765dc517a 100644 --- a/src/components/partials/CV/PageCvContent/PageCVContent.tsx +++ b/src/components/partials/CV/PageCvContent/PageCVContent.tsx @@ -1,3 +1,4 @@ +import moment from 'moment'; import Link from 'next/link'; import React, { useState } from 'react'; import UIkit from 'uikit'; @@ -31,7 +32,7 @@ import { StyledLeftColumn, StyledRightColumn, StyledCVExperienceDate, - StyledCVExperienceDescription + StyledCVExperienceDescription, } from 'src/components/partials/CV/PageCvContent/PageCVContent.styles'; import { Button, Icon } from 'src/components/utils'; import { CarouselSwiper } from 'src/components/utils/CarouselSwiper'; @@ -43,7 +44,6 @@ import { useIsDesktop } from 'src/hooks/utils'; import { fbEvent } from 'src/lib/fb'; import { gaEvent } from 'src/lib/gtag'; import { addPrefix, findConstantFromValue, sortByOrder } from 'src/utils'; -import moment from 'moment'; import 'moment/locale/fr'; interface openedPanelType { @@ -62,10 +62,6 @@ export const PageCVContent = ({ cv, actionDisabled = false, }: PageCVContentProps) => { - - - console.log(cv); - const locations = cv.locations && cv.locations.length > 0 ? sortByOrder(cv.locations) : []; @@ -355,30 +351,37 @@ export const PageCVContent = ({

      - {cv.experiences.map((experience) => { + {cv.experiences?.map((experience) => { return ( - {experience.dateStart && <> - { - experience.dateEnd ? - moment(experience.dateEnd).format(('MMMM YYYY')) : - "Aujourd'hui" - } -
      - {moment(experience.dateStart).format(('MMMM YYYY'))} - } + {experience.dateStart && ( + <> + {experience.dateEnd + ? moment(experience.dateEnd).format('MMMM YYYY') + : "Aujourd'hui"} +
      + {moment(experience.dateStart).format('MMMM YYYY')} + + )}
      - {experience.title &&
      } - { - (experience.company || experience.location) && -
      {experience.company}{experience.company && experience.location && ' - '}{experience.location}
      - } - {experience.description &&
      {experience.description}
      } + {experience.title && ( +
      + )} + {(experience.company || experience.location) && ( +
      + {experience.company} + {experience.company && experience.location && ' - '} + {experience.location} +
      + )} + {experience.description && ( +
      {experience.description}
      + )}
      {experience.skills.map(({ name, id }) => { return ( @@ -416,26 +419,35 @@ export const PageCVContent = ({ return ( - {formation.dateStart && <> - { - formation.dateEnd ? - moment(formation.dateEnd).format(('MMMM YYYY')) : - "Aujourd'hui" - } -
      - {moment(formation.dateStart).format(('MMMM YYYY'))} - } + {formation.dateStart && ( + <> + {formation.dateEnd + ? moment(formation.dateEnd).format('MMMM YYYY') + : "Aujourd'hui"} +
      + {moment(formation.dateStart).format('MMMM YYYY')} + + )}
      - {formation.title &&
      } - { - (formation.institution || formation.location) && -
      {formation.institution}{formation.institution && formation.location && ' - '}{formation.location}
      - } - {formation.description &&
      {formation.description}
      } + {formation.title && ( +
      + )} + {(formation.institution || formation.location) && ( +
      + {formation.institution} + {formation.institution && + formation.location && + ' - '} + {formation.location} +
      + )} + {formation.description && ( +
      {formation.description}
      + )}
      {formation.skills.map(({ name, id }) => { return ( diff --git a/src/components/utils/Inputs/DatePicker/DatePicker.tsx b/src/components/utils/Inputs/DatePicker/DatePicker.tsx index c0e7b83bd..25b6294b9 100644 --- a/src/components/utils/Inputs/DatePicker/DatePicker.tsx +++ b/src/components/utils/Inputs/DatePicker/DatePicker.tsx @@ -27,9 +27,6 @@ export function DatePicker({ return null; } - - const valueToDisplay = value?.includes('T') ? value.slice(0, value.indexOf('T')) : value; - if (!min) min = '1900-01-01'; return ( @@ -46,7 +43,7 @@ export function DatePicker({ data-testid={id} className={`${!value ? 'empty-value' : ''}`} name={name} - value={valueToDisplay || ''} + value={value || ''} min={min} max={max} type="date" diff --git a/src/components/utils/Inputs/Selects/SelectCreatable/SelectCreatable.tsx b/src/components/utils/Inputs/Selects/SelectCreatable/SelectCreatable.tsx index 7434dd593..47b41e103 100644 --- a/src/components/utils/Inputs/Selects/SelectCreatable/SelectCreatable.tsx +++ b/src/components/utils/Inputs/Selects/SelectCreatable/SelectCreatable.tsx @@ -1,6 +1,11 @@ -import React from 'react'; +import React, { useState } from 'react'; import CreatableSelect from 'react-select/creatable'; -import { StyledInputLabel } from '../../Inputs.styles'; +import { + StyledAnnotations, + StyledAnnotationsErrorMessage, + StyledInputLabel, + StyledLimit, +} from '../../Inputs.styles'; import { CommonInputProps } from '../../Inputs.types'; import { ClearIndicator, @@ -9,7 +14,6 @@ import { } from '../Selects'; import { StyledSelect, StyledSelectContainer } from '../Selects.styles'; import { IsArrayFilterConstant } from 'src/components/forms/FormSchema'; -import { FieldErrorMessage } from 'src/components/forms/fields/FieldErrorMessage/FieldErrorMessage'; import { FilterConstant } from 'src/constants/utils'; interface SelectAsyncProps @@ -17,6 +21,8 @@ interface SelectAsyncProps options: IsArrayFilterConstant; isMulti?: boolean; openMenuOnClick?: boolean; + maxChar?: number; + maxItems?: number; } export function SelectCreatable({ id, @@ -34,11 +40,24 @@ export function SelectCreatable({ openMenuOnClick = true, showLabel = false, inputRef, + maxChar, + maxItems, }: SelectAsyncProps) { + const [remainingItems, setRemainingItems] = useState(maxItems); + if (hidden) { return null; } + const handleChange = (selectedOptions) => { + setRemainingItems( + selectedOptions ? maxItems - selectedOptions.length : maxItems + ); + if (remainingItems >= 0) { + onChange(selectedOptions); + } + }; + return ( {showLabel && ( @@ -61,16 +80,37 @@ export function SelectCreatable({ : placeholder || title } isDisabled={disabled} - onChange={onChange} + onChange={(selectedOptions) => handleChange(selectedOptions)} onBlur={onBlur} openMenuOnClick={openMenuOnClick} formatCreateLabel={(userInput) => { return `Créer "${userInput}"`; }} ref={inputRef} + max={maxItems} + maxLength={maxChar} + isValidNewOption={(inputValue) => { + return maxChar ? inputValue.length < maxChar : true; + }} /> - + +
      + +
      + {maxChar && ( + + + Chaque élément ne doit pas dépasser {maxChar} caractères. + + + )} + {maxItems && ( + + {remainingItems} élément(s) restant(s) + + )} +
      ); } diff --git a/src/utils/Sorting.ts b/src/utils/Sorting.ts index f20406924..63d5080b6 100644 --- a/src/utils/Sorting.ts +++ b/src/utils/Sorting.ts @@ -14,11 +14,12 @@ export function sortByName(list: T) { return listToSort; } - export function sortByDateStart(list: T) { const listToSort = JSON.parse(JSON.stringify(list)); listToSort.sort((a, b) => { - return a.dateStart?.localeCompare(b.dateStart) > 0 ? -1 : 1; + if (typeof a.dateStart === 'string') a.dateStart = Date.parse(a.dateStart); + if (typeof b.dateStart === 'string') b.dateStart = Date.parse(b.dateStart); + return b.dateStart - a.dateStart; }); return listToSort; }