js: display estimated duration in champ editor

This commit is contained in:
Pierre de La Morinerie 2022-05-25 14:48:32 +00:00
parent 3bd637fc56
commit c738d7d07f
8 changed files with 121 additions and 21 deletions

View file

@ -143,4 +143,19 @@
padding-bottom: 15px;
padding-top: 15px;
}
.fill-duration {
align-self: center;
font-size: 14px;
a {
text-decoration: underline;
text-decoration-style: dotted;
// Remove the icon indicating an external link (for less visual noise)
&[target="_blank"]::after {
display: none;
}
}
}
}

View file

@ -37,7 +37,8 @@ module ProcedureHelper
typeDeChamps: procedure.draft_revision.types_de_champ_public_as_json,
baseUrl: admin_procedure_types_de_champ_path(procedure),
directUploadUrl: rails_direct_uploads_url,
continuerUrl: admin_procedure_path(procedure)
continuerUrl: admin_procedure_path(procedure),
estimatedFillDuration: procedure.feature_enabled?(:procedure_estimate_fill_duration) ? procedure.draft_revision.estimated_fill_duration : 0
}
end

View file

@ -93,7 +93,13 @@ export const TypeDeChampComponent = SortableElement<TypeDeChampProps>(
if (confirm('Êtes vous sûr de vouloir supprimer ce champ ?'))
dispatch({
type: 'removeTypeDeChamp',
params: { typeDeChamp }
params: { typeDeChamp },
done: (estimatedFillDuration) => {
dispatch({
type: 'refresh',
params: { estimatedFillDuration }
});
}
});
}}
>
@ -223,7 +229,12 @@ function createUpdateHandler(
field,
value: readValue(target)
},
done: () => dispatch({ type: 'refresh' })
done: (estimatedFillDuration: number) => {
return dispatch({
type: 'refresh',
params: { estimatedFillDuration }
});
}
})
};
}

View file

@ -45,7 +45,8 @@ export function TypeDeChampRepetitionOptions({
dispatch({
type: 'addNewRepetitionTypeDeChamp',
params: { typeDeChamp },
done: () => dispatch({ type: 'refresh' })
done: (estimatedFillDuration: number) =>
dispatch({ type: 'refresh', params: { estimatedFillDuration } })
})
}
>

View file

@ -24,6 +24,10 @@ export function TypeDeChamps({
(tdc) => tdc.id == undefined
);
const formattedEstimatedFillDuration = state.estimatedFillDuration
? Math.max(1, Math.round(state.estimatedFillDuration / 60)) + ' mn'
: '';
return (
<div className="champs-editor">
<SortableContainer
@ -60,7 +64,12 @@ export function TypeDeChamps({
onClick={() =>
dispatch({
type: 'addNewTypeDeChamp',
done: () => dispatch({ type: 'refresh' })
done: (estimatedFillDuration: number) => {
dispatch({
type: 'refresh',
params: { estimatedFillDuration }
});
}
})
}
>
@ -68,6 +77,18 @@ export function TypeDeChamps({
&nbsp;&nbsp;
{addChampLabel(state.isAnnotation)}
</button>
{state.estimatedFillDuration > 0 && (
<span className="fill-duration">
Durée de remplissage estimée&nbsp;:{' '}
<a
href="https://doc.demarches-simplifiees.fr/tutoriels/tutoriel-administrateur#g.-estimation-de-la-duree-de-remplissage"
target="_blank"
rel="noopener noreferrer"
>
{formattedEstimatedFillDuration}
</a>
</span>
)}
<a className="button accepted" href={state.continuerUrl}>
Continuer &gt;
</a>

View file

@ -12,6 +12,7 @@ type TypesDeChampEditorProps = {
isAnnotation: boolean;
typeDeChamps: TypeDeChamp[];
typeDeChampsTypes: [label: string, type: string][];
estimatedFillDuration: number;
};
export type State = Omit<TypesDeChampEditorProps, 'baseUrl'> & {
@ -27,6 +28,7 @@ export type State = Omit<TypesDeChampEditorProps, 'baseUrl'> & {
| 'mandatory'
>;
prefix?: string;
estimatedFillDuration?: number;
};
export default function TypesDeChampEditor(props: TypesDeChampEditorProps) {
@ -47,7 +49,8 @@ export default function TypesDeChampEditor(props: TypesDeChampEditorProps) {
typeDeChampsTypes: props.typeDeChampsTypes,
directUploadUrl: props.directUploadUrl,
isAnnotation: props.isAnnotation,
continuerUrl: props.continuerUrl
continuerUrl: props.continuerUrl,
estimatedFillDuration: props.estimatedFillDuration
};
return <TypeDeChamps state={state} typeDeChamps={props.typeDeChamps} />;

View file

@ -3,19 +3,20 @@ import {
createTypeDeChampOperation,
destroyTypeDeChampOperation,
moveTypeDeChampOperation,
updateTypeDeChampOperation
updateTypeDeChampOperation,
estimateFillDuration
} from './operations';
import type { TypeDeChamp, State, Flash, OperationsQueue } from './types';
type AddNewTypeDeChampAction = {
type: 'addNewTypeDeChamp';
done: () => void;
done: (estimatedFillDuration: number) => void;
};
type AddNewRepetitionTypeDeChampAction = {
type: 'addNewRepetitionTypeDeChamp';
params: { typeDeChamp: TypeDeChamp };
done: () => void;
done: (estimatedFillDuration: number) => void;
};
type UpdateTypeDeChampAction = {
@ -25,12 +26,13 @@ type UpdateTypeDeChampAction = {
field: keyof TypeDeChamp;
value: string | boolean;
};
done: () => void;
done: (estimatedFillDuration: number) => void;
};
type RemoveTypeDeChampAction = {
type: 'removeTypeDeChamp';
params: { typeDeChamp: TypeDeChamp };
done: (estimatedFillDuration: number) => void;
};
type MoveTypeDeChampUpAction = {
@ -50,6 +52,7 @@ type OnSortTypeDeChampsAction = {
type RefreshAction = {
type: 'refresh';
params: { estimatedFillDuration: number };
};
export type Action =
@ -84,7 +87,12 @@ export default function typeDeChampsReducer(
action.done
);
case 'removeTypeDeChamp':
return removeTypeDeChamp(state, state.typeDeChamps, action.params);
return removeTypeDeChamp(
state,
state.typeDeChamps,
action.params,
action.done
);
case 'moveTypeDeChampUp':
return moveTypeDeChampUp(state, state.typeDeChamps, action.params);
case 'moveTypeDeChampDown':
@ -92,7 +100,11 @@ export default function typeDeChampsReducer(
case 'onSortTypeDeChamps':
return onSortTypeDeChamps(state, state.typeDeChamps, action.params);
case 'refresh':
return { ...state, typeDeChamps: [...state.typeDeChamps] };
return {
...state,
typeDeChamps: [...state.typeDeChamps],
estimatedFillDuration: action.params.estimatedFillDuration
};
}
}
@ -100,7 +112,7 @@ function addTypeDeChamp(
state: State,
typeDeChamps: TypeDeChamp[],
insertAfter: { index: number; target: HTMLDivElement } | null,
done: () => void
done: (estimatedFillDuration: number) => void
) {
const typeDeChamp = {
...state.defaultTypeDeChampAttributes
@ -117,7 +129,8 @@ function addTypeDeChamp(
);
}
state.flash.success();
done();
const estimatedFillDuration = await estimateFillDuration(state.queue);
done(estimatedFillDuration);
if (insertAfter) {
insertAfter.target.nextElementSibling?.scrollIntoView({
behavior: 'smooth',
@ -150,7 +163,7 @@ function addTypeDeChamp(
function addNewTypeDeChamp(
state: State,
typeDeChamps: TypeDeChamp[],
done: () => void
done: (estimatedFillDuration: number) => void
) {
return addTypeDeChamp(state, typeDeChamps, findItemToInsertAfter(), done);
}
@ -159,7 +172,7 @@ function addNewRepetitionTypeDeChamp(
state: State,
typeDeChamps: TypeDeChamp[],
{ typeDeChamp }: AddNewRepetitionTypeDeChampAction['params'],
done: () => void
done: (estimatedFillDuration: number) => void
) {
return addTypeDeChamp(
{
@ -179,7 +192,7 @@ function updateTypeDeChamp(
state: State,
typeDeChamps: TypeDeChamp[],
{ typeDeChamp, field, value }: UpdateTypeDeChampAction['params'],
done: () => void
done: (estimatedFillDuration: number) => void
) {
if (field == 'type_champ' && !typeDeChamp.drop_down_list_value) {
switch (value) {
@ -212,10 +225,17 @@ function updateTypeDeChamp(
function removeTypeDeChamp(
state: State,
typeDeChamps: TypeDeChamp[],
{ typeDeChamp }: RemoveTypeDeChampAction['params']
{ typeDeChamp }: RemoveTypeDeChampAction['params'],
done: (estimatedFillDuration: number) => void
) {
destroyTypeDeChampOperation(typeDeChamp, state.queue)
.then(() => state.flash.success())
.then(() => {
state.flash.success();
return estimateFillDuration(state.queue);
})
.then((estimatedFillDuration: number) => {
done(estimatedFillDuration);
})
.catch((message) => state.flash.error(message));
return {
@ -295,11 +315,14 @@ function getUpdateHandler(
let handler = updateHandlers.get(typeDeChamp);
if (!handler) {
handler = debounce(
(done: () => void) =>
(done: (estimatedFillDuration: number) => void) =>
updateTypeDeChampOperation(typeDeChamp, queue)
.then(() => {
flash.success();
done();
return estimateFillDuration(queue);
})
.then((estimatedFillDuration: number) => {
done(estimatedFillDuration);
})
.catch((message) => flash.error(message)),
200

View file

@ -143,4 +143,29 @@ describe 'As an administrateur I can edit types de champ', js: true do
expect(page).to have_content('Un menu')
end
context "when the estimated fill duration is enabled" do
before { Flipper.enable(:procedure_estimated_fill_duration) }
after { Flipper.disable(:procedure_estimated_fill_duration) }
scenario "displaying the estimated fill duration" do
# It doesn't display anything when there are no champs
expect(page).not_to have_content('Durée de remplissage estimé')
# It displays the estimate when adding a new champ
add_champ
select('Pièce justificative', from: 'champ-0-type_champ')
expect(page).to have_content('Durée de remplissage estimée : 1 mn')
# It updates the estimate when updating the champ
check 'Obligatoire'
expect(page).to have_content('Durée de remplissage estimée : 3 mn')
# It updates the estimate when removing the champ
page.accept_alert do
click_on 'Supprimer'
end
expect(page).not_to have_content('Durée de remplissage estimée')
end
end
end