Merge pull request #523 from betagouv/améliorations-diverses

Petite refacto de sélecteurs et ravalement du retour utilisateur
pull/478/head
Mael 2019-04-23 12:12:18 +02:00 committed by GitHub
commit e86b7c1f43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 181 additions and 141 deletions

View File

@ -1,8 +1,23 @@
.feedback-page {
align-items: flex-start;
display: flex;
align-items: flex-end;
justify-content: space-between;
justify-content: flex-end;
padding-top: 0.6rem;
padding-bottom: 0.6rem;
background: var(--lighterColour);
border-radius: 0.9rem;
padding: 0.6em 1em;
}
.feedback-page.stickToFooter {
margin-bottom: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.feedback-page button.link-button {
margin: 0 0.6em;
}
@media (min-width: 1200px) {
.feedback-page .feedbackButtons {
display: inline;
}
}

View File

@ -6,7 +6,7 @@ import React, { Component } from 'react'
import { Trans } from 'react-i18next'
import type { Tracker } from 'Components/utils/withTracker'
type Props = { onEnd: () => void, tracker: Tracker }
type Props = { onEnd: () => void, tracker: Tracker, onCancel: () => void }
class FeedbackForm extends Component<Props> {
formRef: ?HTMLFormElement
@ -23,9 +23,6 @@ class FeedbackForm extends Component<Props> {
// $FlowFixMe
body: new FormData(this.formRef)
})
this.handleClose()
}
handleClose = () => {
this.props.onEnd()
}
@ -34,7 +31,7 @@ class FeedbackForm extends Component<Props> {
<ScrollToElement onlyIfNotVisible>
<div style={{ textAlign: 'end' }}>
<button
onClick={this.handleClose}
onClick={() => this.props.onCancel()}
className="ui__ link-button"
style={{ textDecoration: 'none', marginLeft: '0.3rem' }}
aria-label="close">

View File

@ -11,11 +11,13 @@ import Form from './FeedbackForm'
import type { Tracker } from 'Components/utils/withTracker'
import type { Location } from 'react-router-dom'
import type { Node } from 'react'
import classNames from 'classnames'
type OwnProps = {
blacklist: Array<string>,
customMessage?: Node,
customEventName?: string
customEventName?: string,
stickToFooter: boolean
}
type Props = OwnProps & {
location: Location,
@ -89,6 +91,7 @@ class PageFeedback extends Component<Props, State> {
this.setState({ showForm: true })
}
render() {
let { stickToFooter = false } = this.props
if (this.feedbackAlreadyGiven) {
return null
}
@ -96,56 +99,63 @@ class PageFeedback extends Component<Props, State> {
this.props.location.pathname === '/' ? '' : this.props.location.pathname
return (
!this.props.blacklist.includes(pathname) && (
<div className="feedback-page ui__ container notice">
{!this.state.showForm && !this.state.showThanks && (
<>
<div>
{this.props.customMessage || (
<Trans i18nKey="feedback.question">
Cette page vous est utile ?
</Trans>
)}{' '}
<div style={{ display: 'inline-block' }}>
<button
style={{ marginLeft: '0.4rem' }}
className="ui__ link-button"
onClick={() => this.handleFeedback({ useful: true })}>
<Trans>Oui</Trans>
</button>{' '}
<button
style={{ marginLeft: '0.4rem' }}
className="ui__ link-button"
onClick={() => this.handleFeedback({ useful: false })}>
<Trans>Non</Trans>
</button>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<div
className={classNames('feedback-page', 'ui__', 'notice', {
stickToFooter
})}>
{!this.state.showForm && !this.state.showThanks && (
<>
<div>
{this.props.customMessage || (
<Trans i18nKey="feedback.question">
Cette page vous est utile ?
</Trans>
)}{' '}
<div className="feedbackButtons">
<button
className="ui__ link-button"
onClick={() => this.handleFeedback({ useful: true })}>
<Trans>Oui</Trans>
</button>{' '}
<button
className="ui__ link-button"
onClick={() => this.handleFeedback({ useful: false })}>
<Trans>Non</Trans>
</button>
<button
className="ui__ link-button"
onClick={this.handleErrorReporting}>
<Trans i18nKey="feedback.reportError">
Faire une suggestion
</Trans>
</button>
</div>
</div>
</div>
<button
style={{ textAlign: 'right' }}
className="ui__ link-button"
onClick={this.handleErrorReporting}>
<Trans i18nKey="feedback.reportError">
Faire une suggestion
</>
)}
{this.state.showThanks && (
<div>
<Trans i18nKey="feedback.thanks">
Merci pour votre retour ! Vous pouvez nous contacter
directement à{' '}
<a href="mailto:contact@embauche.beta.gouv.fr">
contact@embauche.beta.gouv.fr
</a>
</Trans>
</button>{' '}
</>
)}
{this.state.showThanks && (
<div>
<Trans i18nKey="feedback.thanks">
Merci pour votre retour ! Vous pouvez nous contacter directement
à{' '}
<a href="mailto:contact@embauche.beta.gouv.fr">
contact@embauche.beta.gouv.fr
</a>
</Trans>
</div>
)}
{this.state.showForm && (
<Form
onEnd={() => this.setState({ showThanks: true, showForm: false })}
/>
)}
</div>
)}
{this.state.showForm && (
<Form
onEnd={() =>
this.setState({ showThanks: false, showForm: false })
}
onCancel={() =>
this.setState({ showThanks: false, showForm: false })
}
/>
)}
</div>
</div>
)
)

View File

@ -7,7 +7,6 @@ import { Trans } from 'react-i18next'
import { connect } from 'react-redux'
import { withRouter } from 'react-router'
import { animated, Spring } from 'react-spring'
import { noUserInputSelector } from 'Selectors/analyseSelectors'
import type { Location } from 'react-router'
type OwnProps = {
@ -16,17 +15,10 @@ type OwnProps = {
type Props = OwnProps & {
startConversation: (?string) => void,
location: Location,
userInput: boolean,
conversationStarted: boolean
show: boolean
}
const QuickLinks = ({
startConversation,
userInput,
quickLinks,
conversationStarted
}: Props) => {
const show = userInput && !conversationStarted
const QuickLinks = ({ startConversation, show, quickLinks }: Props) => {
return (
<Spring
to={{
@ -45,7 +37,7 @@ const QuickLinks = ({
flexWrap: 'wrap-reverse',
fontSize: '110%',
justifyContent: 'space-evenly',
marginBottom: '0.6rem'
marginBottom: '1rem'
}}>
{toPairs(quickLinks).map(([label, dottedName]) => (
<button
@ -67,8 +59,6 @@ export default (compose(
connect(
(state, props) => ({
key: props.language,
userInput: !noUserInputSelector(state),
conversationStarted: state.conversationStarted,
quickLinks: state.simulation?.config["questions à l'affiche"]
}),
{

View File

@ -1,7 +1,7 @@
import { startConversation } from 'Actions/actions'
import withTracker from 'Components/utils/withTracker'
import { compose } from 'ramda'
import React, { Component } from 'react'
import { React, T } from 'Components'
import { connect } from 'react-redux'
import { formValueSelector } from 'redux-form'
import ficheDePaieSelectors from 'Selectors/ficheDePaieSelectors'
@ -24,10 +24,18 @@ export default compose(
}
)
)(
class SalaryExplanation extends Component {
class SalaryExplanation extends React.Component {
render() {
return (
<Animate.fromBottom delay={2000}>
<p>
<T k="simulateurs.salarié.description">
Dès que le salarié est déclaré et payé, il est couvert par le
régime général de la Sécurité sociale (santé, maternité,
invalidité, vieillesse, maladie professionnelle et accidents) et
chômage.
</T>
</p>
{!this.props.conversationStarted ? (
<SalaryFirstExplanation {...this.props} />
) : (

View File

@ -45,8 +45,7 @@ export default compose(
let arePreviousAnswers = previousAnswers.length > 0,
displayConversation =
!targetsTriggerConversation || conversationStarted,
showTargets =
targetsTriggerConversation || !noUserInput || showTargetsAnyway
showTargets = targetsTriggerConversation || showTargetsAnyway
return (
<>
{this.state.displayAnswers && (
@ -101,9 +100,8 @@ export default compose(
)}
{showTargets && this.props.targets}
{!noUserInput && this.props.explanation}
{!noUserInput && !noFeedback && (
<div style={{ margin: '-0.6rem' }}>
{!noFeedback && (
<div>
<PageFeedback
customMessage={
<T k="feedback.simulator">

View File

@ -14,14 +14,14 @@ import { Link } from 'react-router-dom'
import { change, Field, formValueSelector, reduxForm } from 'redux-form'
import {
analysisWithDefaultsSelector,
flatRulesSelector,
noUserInputSelector
flatRulesSelector
} from 'Selectors/analyseSelectors'
import Animate from 'Ui/animate'
import AnimatedTargetValue from 'Ui/AnimatedTargetValue'
import CurrencyInput from './CurrencyInput/CurrencyInput'
import QuickLinks from './QuickLinks'
import './TargetSelection.css'
import { firstStepCompletedSelector } from './targetSelectionSelectors'
export default compose(
withTranslation(),
@ -36,7 +36,7 @@ export default compose(
formValueSelector('conversation')(state, dottedName),
analysis: analysisWithDefaultsSelector(state),
flatRules: flatRulesSelector(state),
noUserInput: noUserInputSelector(state),
firstStepCompleted: firstStepCompletedSelector(state),
conversationStarted: state.conversationStarted,
activeInput: state.activeTargetInput,
objectifs: state.simulation?.config.objectifs || []
@ -80,12 +80,18 @@ export default compose(
}
}
render() {
let { colours, noUserInput, analysis } = this.props,
let {
colours,
firstStepCompleted,
conversationStarted,
analysis,
explanation
} = this.props,
inversionFail = analysis.cache.inversionFail
return (
<div id="targetSelection">
{!noUserInput && (
{firstStepCompleted && (
<Controls
inversionFail={inversionFail}
controls={analysis.controls}
@ -105,7 +111,8 @@ export default compose(
}}>
{this.renderOutputList()}
</section>
<QuickLinks />
<QuickLinks show={firstStepCompleted && !conversationStarted} />
{firstStepCompleted && explanation}
</div>
)
}
@ -258,7 +265,7 @@ let TargetInputOrValue = withLanguage(
activeInput,
setActiveInput,
language,
noUserInput,
firstStepCompleted,
inversionFail
}) => (
<span className="targetInputOrValue">
@ -278,7 +285,7 @@ let TargetInputOrValue = withLanguage(
target,
activeInput,
setActiveInput,
noUserInput,
firstStepCompleted,
inversionFail
}}
/>

View File

@ -0,0 +1,32 @@
import { createSelector } from 'reselect'
import {
formattedSituationSelector,
targetNamesSelector,
parsedRulesSelector
} from 'Selectors/analyseSelectors'
import { findRuleByDottedName } from 'Engine/rules'
export let firstStepCompletedSelector = createSelector(
[
formattedSituationSelector,
targetNamesSelector,
parsedRulesSelector,
state => state.simulation?.config?.bloquant
],
(situation, targetNames, parsedRules, bloquant) => {
if (!situation) {
return true
}
const situations = Object.keys(situation)
const allBlockingAreAnswered =
bloquant && bloquant.every(rule => situations.includes(rule))
const targetIsAnswered =
targetNames &&
targetNames.some(
targetName =>
findRuleByDottedName(parsedRules, targetName)?.formule &&
targetName in situation
)
return allBlockingAreAnswered || targetIsAnswered
}
)

View File

@ -21,7 +21,9 @@ import {
intersection,
isNil,
mergeDeepWith,
pick
pick,
isEmpty,
dissoc
} from 'ramda'
import { getFormValues } from 'redux-form'
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'
@ -65,28 +67,8 @@ export let formattedSituationSelector = createSelector(
)
export let noUserInputSelector = createSelector(
[
formattedSituationSelector,
targetNamesSelector,
parsedRulesSelector,
state => state.simulation?.config?.bloquant
],
(situation, targetNames, parsedRules, bloquant) => {
if (!situation) {
return true
}
const situations = Object.keys(situation)
const allBlockingAreAnswered =
bloquant && bloquant.every(rule => situations.includes(rule))
const targetIsAnswered =
targetNames &&
targetNames.some(
targetName =>
findRuleByDottedName(parsedRules, targetName)?.formule &&
targetName in situation
)
return !(allBlockingAreAnswered || targetIsAnswered)
}
[formattedSituationSelector],
situation => !situation || isEmpty(dissoc('période', situation))
)
let validatedStepsSelector = createSelector(

View File

@ -60,6 +60,7 @@ const Footer = ({ colours: { colour }, tracker, t, sitePaths }) => {
))}
</Helmet>
<PageFeedback
stickToFooter={true}
blacklist={feedbackBlacklist.map(lens => view(lens, sitePaths))}
/>
<footer className="footer" style={{ backgroundColor: `${colour}22` }}>

View File

@ -36,20 +36,23 @@ const AssimiléSalarié = ({ t }) => (
<Warning />
<Simulation
targetsTriggerConversation={true}
targets={<TargetSelection />}
explanation={
<>
<p>
{emoji('☂️ ')}{' '}
<T k="simulateurs.assimilé-salarié.explications">
Les gérants égalitaires ou minoritaires de SARL ou les dirigeants
de SA et SAS sont assimilés salariés et relèvent du régime
général. Par conséquent, le dirigeant a la même protection sociale
qu'un salarié, mis à part le chômage.
</T>
</p>
<SalaryExplanation />
</>
targets={
<TargetSelection
explanation={
<>
<p>
{emoji('☂️ ')}{' '}
<T k="simulateurs.assimilé-salarié.explications">
Les gérants égalitaires ou minoritaires de SARL ou les
dirigeants de SA et SAS sont assimilés salariés et relèvent du
régime général. Par conséquent, le dirigeant a la même
protection sociale qu'un salarié, mis à part le chômage.
</T>
</p>
<SalaryExplanation />
</>
}
/>
}
/>
</>

View File

@ -35,8 +35,11 @@ const AutoEntrepreneur = ({ t }) => (
<Warning simulateur="auto-entreprise" />
<Simulation
targetsTriggerConversation={true}
targets={<TargetSelection />}
explanation={<AvertissementProtectionSocialeIndépendants />}
targets={
<TargetSelection
explanation={<AvertissementProtectionSocialeIndépendants />}
/>
}
/>
</>
)

View File

@ -35,12 +35,15 @@ const Indépendant = ({ t }) => (
<Warning />
<Simulation
targetsTriggerConversation={true}
targets={<TargetSelection />}
explanation={
<>
<AvertissementForfaitIndépendants />
<AvertissementProtectionSocialeIndépendants />
</>
targets={
<TargetSelection
explanation={
<>
<AvertissementForfaitIndépendants />
<AvertissementProtectionSocialeIndépendants />
</>
}
/>
}
/>
</>

View File

@ -1,5 +1,4 @@
import { React, T } from 'Components'
import SalaryExplanation from 'Components/SalaryExplanation'
import Simulation from 'Components/Simulation'
import salariéConfig from 'Components/simulationConfigs/salarié.yaml'
import withSimulationConfig from 'Components/simulationConfigs/withSimulationConfig'
@ -9,6 +8,7 @@ import { compose } from 'ramda'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import SalaryExplanation from 'Components/SalaryExplanation'
export let SalarySimulation = withSitePaths(({ sitePaths }) => (
<Simulation
@ -29,8 +29,7 @@ export let SalarySimulation = withSitePaths(({ sitePaths }) => (
)}
</>
}
targets={<TargetSelection />}
explanation={<SalaryExplanation />}
targets={<TargetSelection explanation={<SalaryExplanation />} />}
/>
))
@ -55,14 +54,6 @@ const Salarié = ({ t }) => (
<T k="simulateurs.salarié.titre">Simulateur de salaire</T>
</h1>
<SalarySimulation />
<p>
<T k="simulateurs.salarié.description">
Dès que l'embauche d'un salarié est déclarée et qu'il est payé, il est
couvert par le régime général de la Sécurité sociale (santé, maternité,
invalidité, vieillesse, maladie professionnelle et accidents) et
chômage.
</T>
</p>
</>
)
export default compose(