js: display estimated duration in champ editor
This commit is contained in:
parent
3bd637fc56
commit
c738d7d07f
8 changed files with 121 additions and 21 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 }
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
|
|
@ -45,7 +45,8 @@ export function TypeDeChampRepetitionOptions({
|
|||
dispatch({
|
||||
type: 'addNewRepetitionTypeDeChamp',
|
||||
params: { typeDeChamp },
|
||||
done: () => dispatch({ type: 'refresh' })
|
||||
done: (estimatedFillDuration: number) =>
|
||||
dispatch({ type: 'refresh', params: { estimatedFillDuration } })
|
||||
})
|
||||
}
|
||||
>
|
||||
|
|
|
@ -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({
|
|||
|
||||
{addChampLabel(state.isAnnotation)}
|
||||
</button>
|
||||
{state.estimatedFillDuration > 0 && (
|
||||
<span className="fill-duration">
|
||||
Durée de remplissage estimée :{' '}
|
||||
<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 >
|
||||
</a>
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue