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-bottom: 15px;
|
||||||
padding-top: 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,
|
typeDeChamps: procedure.draft_revision.types_de_champ_public_as_json,
|
||||||
baseUrl: admin_procedure_types_de_champ_path(procedure),
|
baseUrl: admin_procedure_types_de_champ_path(procedure),
|
||||||
directUploadUrl: rails_direct_uploads_url,
|
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
|
end
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,13 @@ export const TypeDeChampComponent = SortableElement<TypeDeChampProps>(
|
||||||
if (confirm('Êtes vous sûr de vouloir supprimer ce champ ?'))
|
if (confirm('Êtes vous sûr de vouloir supprimer ce champ ?'))
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'removeTypeDeChamp',
|
type: 'removeTypeDeChamp',
|
||||||
params: { typeDeChamp }
|
params: { typeDeChamp },
|
||||||
|
done: (estimatedFillDuration) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'refresh',
|
||||||
|
params: { estimatedFillDuration }
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -223,7 +229,12 @@ function createUpdateHandler(
|
||||||
field,
|
field,
|
||||||
value: readValue(target)
|
value: readValue(target)
|
||||||
},
|
},
|
||||||
done: () => dispatch({ type: 'refresh' })
|
done: (estimatedFillDuration: number) => {
|
||||||
|
return dispatch({
|
||||||
|
type: 'refresh',
|
||||||
|
params: { estimatedFillDuration }
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,8 @@ export function TypeDeChampRepetitionOptions({
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'addNewRepetitionTypeDeChamp',
|
type: 'addNewRepetitionTypeDeChamp',
|
||||||
params: { typeDeChamp },
|
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
|
(tdc) => tdc.id == undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const formattedEstimatedFillDuration = state.estimatedFillDuration
|
||||||
|
? Math.max(1, Math.round(state.estimatedFillDuration / 60)) + ' mn'
|
||||||
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="champs-editor">
|
<div className="champs-editor">
|
||||||
<SortableContainer
|
<SortableContainer
|
||||||
|
@ -60,7 +64,12 @@ export function TypeDeChamps({
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'addNewTypeDeChamp',
|
type: 'addNewTypeDeChamp',
|
||||||
done: () => dispatch({ type: 'refresh' })
|
done: (estimatedFillDuration: number) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'refresh',
|
||||||
|
params: { estimatedFillDuration }
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -68,6 +77,18 @@ export function TypeDeChamps({
|
||||||
|
|
||||||
{addChampLabel(state.isAnnotation)}
|
{addChampLabel(state.isAnnotation)}
|
||||||
</button>
|
</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}>
|
<a className="button accepted" href={state.continuerUrl}>
|
||||||
Continuer >
|
Continuer >
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -12,6 +12,7 @@ type TypesDeChampEditorProps = {
|
||||||
isAnnotation: boolean;
|
isAnnotation: boolean;
|
||||||
typeDeChamps: TypeDeChamp[];
|
typeDeChamps: TypeDeChamp[];
|
||||||
typeDeChampsTypes: [label: string, type: string][];
|
typeDeChampsTypes: [label: string, type: string][];
|
||||||
|
estimatedFillDuration: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type State = Omit<TypesDeChampEditorProps, 'baseUrl'> & {
|
export type State = Omit<TypesDeChampEditorProps, 'baseUrl'> & {
|
||||||
|
@ -27,6 +28,7 @@ export type State = Omit<TypesDeChampEditorProps, 'baseUrl'> & {
|
||||||
| 'mandatory'
|
| 'mandatory'
|
||||||
>;
|
>;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
|
estimatedFillDuration?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TypesDeChampEditor(props: TypesDeChampEditorProps) {
|
export default function TypesDeChampEditor(props: TypesDeChampEditorProps) {
|
||||||
|
@ -47,7 +49,8 @@ export default function TypesDeChampEditor(props: TypesDeChampEditorProps) {
|
||||||
typeDeChampsTypes: props.typeDeChampsTypes,
|
typeDeChampsTypes: props.typeDeChampsTypes,
|
||||||
directUploadUrl: props.directUploadUrl,
|
directUploadUrl: props.directUploadUrl,
|
||||||
isAnnotation: props.isAnnotation,
|
isAnnotation: props.isAnnotation,
|
||||||
continuerUrl: props.continuerUrl
|
continuerUrl: props.continuerUrl,
|
||||||
|
estimatedFillDuration: props.estimatedFillDuration
|
||||||
};
|
};
|
||||||
|
|
||||||
return <TypeDeChamps state={state} typeDeChamps={props.typeDeChamps} />;
|
return <TypeDeChamps state={state} typeDeChamps={props.typeDeChamps} />;
|
||||||
|
|
|
@ -3,19 +3,20 @@ import {
|
||||||
createTypeDeChampOperation,
|
createTypeDeChampOperation,
|
||||||
destroyTypeDeChampOperation,
|
destroyTypeDeChampOperation,
|
||||||
moveTypeDeChampOperation,
|
moveTypeDeChampOperation,
|
||||||
updateTypeDeChampOperation
|
updateTypeDeChampOperation,
|
||||||
|
estimateFillDuration
|
||||||
} from './operations';
|
} from './operations';
|
||||||
import type { TypeDeChamp, State, Flash, OperationsQueue } from './types';
|
import type { TypeDeChamp, State, Flash, OperationsQueue } from './types';
|
||||||
|
|
||||||
type AddNewTypeDeChampAction = {
|
type AddNewTypeDeChampAction = {
|
||||||
type: 'addNewTypeDeChamp';
|
type: 'addNewTypeDeChamp';
|
||||||
done: () => void;
|
done: (estimatedFillDuration: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AddNewRepetitionTypeDeChampAction = {
|
type AddNewRepetitionTypeDeChampAction = {
|
||||||
type: 'addNewRepetitionTypeDeChamp';
|
type: 'addNewRepetitionTypeDeChamp';
|
||||||
params: { typeDeChamp: TypeDeChamp };
|
params: { typeDeChamp: TypeDeChamp };
|
||||||
done: () => void;
|
done: (estimatedFillDuration: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UpdateTypeDeChampAction = {
|
type UpdateTypeDeChampAction = {
|
||||||
|
@ -25,12 +26,13 @@ type UpdateTypeDeChampAction = {
|
||||||
field: keyof TypeDeChamp;
|
field: keyof TypeDeChamp;
|
||||||
value: string | boolean;
|
value: string | boolean;
|
||||||
};
|
};
|
||||||
done: () => void;
|
done: (estimatedFillDuration: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RemoveTypeDeChampAction = {
|
type RemoveTypeDeChampAction = {
|
||||||
type: 'removeTypeDeChamp';
|
type: 'removeTypeDeChamp';
|
||||||
params: { typeDeChamp: TypeDeChamp };
|
params: { typeDeChamp: TypeDeChamp };
|
||||||
|
done: (estimatedFillDuration: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MoveTypeDeChampUpAction = {
|
type MoveTypeDeChampUpAction = {
|
||||||
|
@ -50,6 +52,7 @@ type OnSortTypeDeChampsAction = {
|
||||||
|
|
||||||
type RefreshAction = {
|
type RefreshAction = {
|
||||||
type: 'refresh';
|
type: 'refresh';
|
||||||
|
params: { estimatedFillDuration: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
|
@ -84,7 +87,12 @@ export default function typeDeChampsReducer(
|
||||||
action.done
|
action.done
|
||||||
);
|
);
|
||||||
case 'removeTypeDeChamp':
|
case 'removeTypeDeChamp':
|
||||||
return removeTypeDeChamp(state, state.typeDeChamps, action.params);
|
return removeTypeDeChamp(
|
||||||
|
state,
|
||||||
|
state.typeDeChamps,
|
||||||
|
action.params,
|
||||||
|
action.done
|
||||||
|
);
|
||||||
case 'moveTypeDeChampUp':
|
case 'moveTypeDeChampUp':
|
||||||
return moveTypeDeChampUp(state, state.typeDeChamps, action.params);
|
return moveTypeDeChampUp(state, state.typeDeChamps, action.params);
|
||||||
case 'moveTypeDeChampDown':
|
case 'moveTypeDeChampDown':
|
||||||
|
@ -92,7 +100,11 @@ export default function typeDeChampsReducer(
|
||||||
case 'onSortTypeDeChamps':
|
case 'onSortTypeDeChamps':
|
||||||
return onSortTypeDeChamps(state, state.typeDeChamps, action.params);
|
return onSortTypeDeChamps(state, state.typeDeChamps, action.params);
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
return { ...state, typeDeChamps: [...state.typeDeChamps] };
|
return {
|
||||||
|
...state,
|
||||||
|
typeDeChamps: [...state.typeDeChamps],
|
||||||
|
estimatedFillDuration: action.params.estimatedFillDuration
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +112,7 @@ function addTypeDeChamp(
|
||||||
state: State,
|
state: State,
|
||||||
typeDeChamps: TypeDeChamp[],
|
typeDeChamps: TypeDeChamp[],
|
||||||
insertAfter: { index: number; target: HTMLDivElement } | null,
|
insertAfter: { index: number; target: HTMLDivElement } | null,
|
||||||
done: () => void
|
done: (estimatedFillDuration: number) => void
|
||||||
) {
|
) {
|
||||||
const typeDeChamp = {
|
const typeDeChamp = {
|
||||||
...state.defaultTypeDeChampAttributes
|
...state.defaultTypeDeChampAttributes
|
||||||
|
@ -117,7 +129,8 @@ function addTypeDeChamp(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
state.flash.success();
|
state.flash.success();
|
||||||
done();
|
const estimatedFillDuration = await estimateFillDuration(state.queue);
|
||||||
|
done(estimatedFillDuration);
|
||||||
if (insertAfter) {
|
if (insertAfter) {
|
||||||
insertAfter.target.nextElementSibling?.scrollIntoView({
|
insertAfter.target.nextElementSibling?.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
|
@ -150,7 +163,7 @@ function addTypeDeChamp(
|
||||||
function addNewTypeDeChamp(
|
function addNewTypeDeChamp(
|
||||||
state: State,
|
state: State,
|
||||||
typeDeChamps: TypeDeChamp[],
|
typeDeChamps: TypeDeChamp[],
|
||||||
done: () => void
|
done: (estimatedFillDuration: number) => void
|
||||||
) {
|
) {
|
||||||
return addTypeDeChamp(state, typeDeChamps, findItemToInsertAfter(), done);
|
return addTypeDeChamp(state, typeDeChamps, findItemToInsertAfter(), done);
|
||||||
}
|
}
|
||||||
|
@ -159,7 +172,7 @@ function addNewRepetitionTypeDeChamp(
|
||||||
state: State,
|
state: State,
|
||||||
typeDeChamps: TypeDeChamp[],
|
typeDeChamps: TypeDeChamp[],
|
||||||
{ typeDeChamp }: AddNewRepetitionTypeDeChampAction['params'],
|
{ typeDeChamp }: AddNewRepetitionTypeDeChampAction['params'],
|
||||||
done: () => void
|
done: (estimatedFillDuration: number) => void
|
||||||
) {
|
) {
|
||||||
return addTypeDeChamp(
|
return addTypeDeChamp(
|
||||||
{
|
{
|
||||||
|
@ -179,7 +192,7 @@ function updateTypeDeChamp(
|
||||||
state: State,
|
state: State,
|
||||||
typeDeChamps: TypeDeChamp[],
|
typeDeChamps: TypeDeChamp[],
|
||||||
{ typeDeChamp, field, value }: UpdateTypeDeChampAction['params'],
|
{ typeDeChamp, field, value }: UpdateTypeDeChampAction['params'],
|
||||||
done: () => void
|
done: (estimatedFillDuration: number) => void
|
||||||
) {
|
) {
|
||||||
if (field == 'type_champ' && !typeDeChamp.drop_down_list_value) {
|
if (field == 'type_champ' && !typeDeChamp.drop_down_list_value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
|
@ -212,10 +225,17 @@ function updateTypeDeChamp(
|
||||||
function removeTypeDeChamp(
|
function removeTypeDeChamp(
|
||||||
state: State,
|
state: State,
|
||||||
typeDeChamps: TypeDeChamp[],
|
typeDeChamps: TypeDeChamp[],
|
||||||
{ typeDeChamp }: RemoveTypeDeChampAction['params']
|
{ typeDeChamp }: RemoveTypeDeChampAction['params'],
|
||||||
|
done: (estimatedFillDuration: number) => void
|
||||||
) {
|
) {
|
||||||
destroyTypeDeChampOperation(typeDeChamp, state.queue)
|
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));
|
.catch((message) => state.flash.error(message));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -295,11 +315,14 @@ function getUpdateHandler(
|
||||||
let handler = updateHandlers.get(typeDeChamp);
|
let handler = updateHandlers.get(typeDeChamp);
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
handler = debounce(
|
handler = debounce(
|
||||||
(done: () => void) =>
|
(done: (estimatedFillDuration: number) => void) =>
|
||||||
updateTypeDeChampOperation(typeDeChamp, queue)
|
updateTypeDeChampOperation(typeDeChamp, queue)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
flash.success();
|
flash.success();
|
||||||
done();
|
return estimateFillDuration(queue);
|
||||||
|
})
|
||||||
|
.then((estimatedFillDuration: number) => {
|
||||||
|
done(estimatedFillDuration);
|
||||||
})
|
})
|
||||||
.catch((message) => flash.error(message)),
|
.catch((message) => flash.error(message)),
|
||||||
200
|
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')
|
expect(page).to have_content('Un menu')
|
||||||
end
|
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
|
end
|
||||||
|
|
Loading…
Add table
Reference in a new issue