refactor(types_de_champ_editor): remove old react editor
This commit is contained in:
parent
9640461464
commit
65bd996f2a
26 changed files with 6 additions and 1629 deletions
|
@ -30,29 +30,6 @@ module ProcedureHelper
|
||||||
procedure.errors.to_hash(full_messages: true).except(:path)
|
procedure.errors.to_hash(full_messages: true).except(:path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def types_de_champ_data(procedure)
|
|
||||||
{
|
|
||||||
isAnnotation: false,
|
|
||||||
typeDeChampsTypes: TypeDeChamp.type_de_champ_types_for(procedure, current_user),
|
|
||||||
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),
|
|
||||||
estimatedFillDuration: procedure.draft_revision.estimated_fill_duration
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def types_de_champ_private_data(procedure)
|
|
||||||
{
|
|
||||||
isAnnotation: true,
|
|
||||||
typeDeChampsTypes: TypeDeChamp.type_de_champ_types_for(procedure, current_user),
|
|
||||||
typeDeChamps: procedure.draft_revision.types_de_champ_private_as_json,
|
|
||||||
baseUrl: admin_procedure_types_de_champ_path(procedure),
|
|
||||||
directUploadUrl: rails_direct_uploads_url,
|
|
||||||
continuerUrl: admin_procedure_path(procedure)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def procedure_auto_archive_date(procedure)
|
def procedure_auto_archive_date(procedure)
|
||||||
I18n.l(procedure.auto_archive_on - 1.day, format: '%-d %B %Y')
|
I18n.l(procedure.auto_archive_on - 1.day, format: '%-d %B %Y')
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
import invariant from 'tiny-invariant';
|
|
||||||
|
|
||||||
export class Flash {
|
|
||||||
element: HTMLDivElement;
|
|
||||||
isAnnotation: boolean;
|
|
||||||
timeout?: number;
|
|
||||||
|
|
||||||
constructor(isAnnotation: boolean) {
|
|
||||||
const element = document.querySelector<HTMLDivElement>('#flash_messages');
|
|
||||||
invariant(element, 'Flash element is required');
|
|
||||||
this.element = element;
|
|
||||||
this.isAnnotation = isAnnotation;
|
|
||||||
}
|
|
||||||
success() {
|
|
||||||
if (this.isAnnotation) {
|
|
||||||
this.add('Annotations privées enregistrées.');
|
|
||||||
} else {
|
|
||||||
this.add('Formulaire enregistré.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
error(message: string) {
|
|
||||||
this.add(message, true);
|
|
||||||
}
|
|
||||||
clear() {
|
|
||||||
this.element.innerHTML = '';
|
|
||||||
}
|
|
||||||
add(message: string, isError = false) {
|
|
||||||
const html = `<div id="flash_message" class="center">
|
|
||||||
<div class="alert alert-fixed ${
|
|
||||||
isError ? 'alert-danger' : 'alert-success'
|
|
||||||
}">
|
|
||||||
${message}
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
this.element.innerHTML = html;
|
|
||||||
|
|
||||||
clearTimeout(this.timeout);
|
|
||||||
this.timeout = setTimeout(() => {
|
|
||||||
this.clear();
|
|
||||||
}, 4000);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
import { httpRequest, ResponseError } from '@utils';
|
|
||||||
import invariant from 'tiny-invariant';
|
|
||||||
|
|
||||||
type Operation = {
|
|
||||||
path: string;
|
|
||||||
method: string;
|
|
||||||
payload?: unknown;
|
|
||||||
resolve: (value: unknown) => void;
|
|
||||||
reject: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class OperationsQueue {
|
|
||||||
queue: Operation[];
|
|
||||||
isRunning = false;
|
|
||||||
baseUrl: string;
|
|
||||||
|
|
||||||
constructor(baseUrl: string) {
|
|
||||||
this.queue = [];
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
if (this.queue.length > 0) {
|
|
||||||
this.isRunning = true;
|
|
||||||
const operation = this.queue.shift();
|
|
||||||
invariant(operation, 'Operation is required');
|
|
||||||
await this.exec(operation);
|
|
||||||
this.run();
|
|
||||||
} else {
|
|
||||||
this.isRunning = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enqueue(operation: Omit<Operation, 'resolve' | 'reject'>) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.queue.push({ ...operation, resolve, reject });
|
|
||||||
if (!this.isRunning) {
|
|
||||||
this.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async exec(operation: Operation) {
|
|
||||||
const { path, method, payload, resolve, reject } = operation;
|
|
||||||
const url = `${this.baseUrl}${path}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const json = payload;
|
|
||||||
const data = await httpRequest(url, { method, json }).json();
|
|
||||||
resolve(data);
|
|
||||||
} catch (e) {
|
|
||||||
handleError(e as ResponseError, reject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleError(
|
|
||||||
{ message, textBody, jsonBody }: ResponseError,
|
|
||||||
reject: (error: string) => void
|
|
||||||
) {
|
|
||||||
if (textBody) {
|
|
||||||
reject(textBody);
|
|
||||||
} else if (jsonBody) {
|
|
||||||
const {
|
|
||||||
errors: [message]
|
|
||||||
} = jsonBody as { errors: string[] };
|
|
||||||
reject(message);
|
|
||||||
} else {
|
|
||||||
reject(message);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { Handler } from '../types';
|
|
||||||
|
|
||||||
export function DescriptionInput({
|
|
||||||
isVisible,
|
|
||||||
handler
|
|
||||||
}: {
|
|
||||||
isVisible: boolean;
|
|
||||||
handler: Handler<HTMLTextAreaElement>;
|
|
||||||
}) {
|
|
||||||
if (isVisible) {
|
|
||||||
return (
|
|
||||||
<div className="cell">
|
|
||||||
<label htmlFor={handler.id}>Description</label>
|
|
||||||
<textarea
|
|
||||||
id={handler.id}
|
|
||||||
name={handler.name}
|
|
||||||
value={handler.value || ''}
|
|
||||||
onChange={handler.onChange}
|
|
||||||
rows={3}
|
|
||||||
cols={40}
|
|
||||||
className="small-margin small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { Handler } from '../types';
|
|
||||||
|
|
||||||
export function LibelleInput({
|
|
||||||
isVisible,
|
|
||||||
handler
|
|
||||||
}: {
|
|
||||||
isVisible: boolean;
|
|
||||||
handler: Handler<HTMLInputElement>;
|
|
||||||
}) {
|
|
||||||
if (isVisible) {
|
|
||||||
return (
|
|
||||||
<div className="cell libelle">
|
|
||||||
<label htmlFor={handler.id}>Libellé</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id={handler.id}
|
|
||||||
name={handler.name}
|
|
||||||
value={handler.value}
|
|
||||||
onChange={handler.onChange}
|
|
||||||
className="small-margin small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { Handler } from '../types';
|
|
||||||
|
|
||||||
export function MandatoryInput({
|
|
||||||
isVisible,
|
|
||||||
handler
|
|
||||||
}: {
|
|
||||||
isVisible: boolean;
|
|
||||||
handler: Handler<HTMLInputElement>;
|
|
||||||
}) {
|
|
||||||
if (isVisible) {
|
|
||||||
return (
|
|
||||||
<div className="cell">
|
|
||||||
<label htmlFor={handler.id}>Obligatoire</label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={handler.id}
|
|
||||||
name={handler.name}
|
|
||||||
checked={!!handler.value}
|
|
||||||
onChange={handler.onChange}
|
|
||||||
className="small-margin small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import React, { MouseEventHandler } from 'react';
|
|
||||||
import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/solid';
|
|
||||||
|
|
||||||
export function MoveButton({
|
|
||||||
isEnabled,
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
onClick
|
|
||||||
}: {
|
|
||||||
isEnabled: boolean;
|
|
||||||
icon: string;
|
|
||||||
title: string;
|
|
||||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="button small move"
|
|
||||||
disabled={!isEnabled}
|
|
||||||
title={title}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{icon == 'arrow-up' ? (
|
|
||||||
<ArrowUpIcon className="icon-size" />
|
|
||||||
) : (
|
|
||||||
<ArrowDownIcon className="icon-size" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,308 +0,0 @@
|
||||||
import React, { Dispatch } from 'react';
|
|
||||||
import { SortableElement, SortableHandle } from 'react-sortable-hoc';
|
|
||||||
import { useInView } from 'react-intersection-observer';
|
|
||||||
import { TrashIcon } from '@heroicons/react/outline';
|
|
||||||
|
|
||||||
import type { Action, TypeDeChamp, State, Handler } from '../types';
|
|
||||||
import { DescriptionInput } from './DescriptionInput';
|
|
||||||
import { LibelleInput } from './LibelleInput';
|
|
||||||
import { MandatoryInput } from './MandatoryInput';
|
|
||||||
import { MoveButton } from './MoveButton';
|
|
||||||
import { TypeDeChampCarteOption } from './TypeDeChampCarteOption';
|
|
||||||
import { TypeDeChampCarteOptions } from './TypeDeChampCarteOptions';
|
|
||||||
import { TypeDeChampDropDownOptions } from './TypeDeChampDropDownOptions';
|
|
||||||
import { TypeDeChampDropDownOther } from './TypeDeChampDropDownOther';
|
|
||||||
import { TypeDeChampPieceJustificative } from './TypeDeChampPieceJustificative';
|
|
||||||
import { TypeDeChampRepetitionOptions } from './TypeDeChampRepetitionOptions';
|
|
||||||
import { TypeDeChampTypesSelect } from './TypeDeChampTypesSelect';
|
|
||||||
import { TypeDeChampDropDownSecondary } from './TypeDeChampDropDownSecondary';
|
|
||||||
|
|
||||||
type TypeDeChampProps = {
|
|
||||||
typeDeChamp: TypeDeChamp;
|
|
||||||
dispatch: Dispatch<Action>;
|
|
||||||
idx: number;
|
|
||||||
isFirstItem: boolean;
|
|
||||||
isLastItem: boolean;
|
|
||||||
state: State;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TypeDeChampComponent = SortableElement<TypeDeChampProps>(
|
|
||||||
({
|
|
||||||
typeDeChamp,
|
|
||||||
dispatch,
|
|
||||||
idx: index,
|
|
||||||
isFirstItem,
|
|
||||||
isLastItem,
|
|
||||||
state
|
|
||||||
}: TypeDeChampProps) => {
|
|
||||||
const isDropDown = [
|
|
||||||
'drop_down_list',
|
|
||||||
'multiple_drop_down_list',
|
|
||||||
'linked_drop_down_list'
|
|
||||||
].includes(typeDeChamp.type_champ);
|
|
||||||
const isLinkedDropDown = typeDeChamp.type_champ === 'linked_drop_down_list';
|
|
||||||
const isSimpleDropDown = typeDeChamp.type_champ === 'drop_down_list';
|
|
||||||
const isFile = typeDeChamp.type_champ === 'piece_justificative';
|
|
||||||
const isCarte = typeDeChamp.type_champ === 'carte';
|
|
||||||
const isExplication = typeDeChamp.type_champ === 'explication';
|
|
||||||
const isHeaderSection = typeDeChamp.type_champ === 'header_section';
|
|
||||||
const isTitreIdentite = typeDeChamp.type_champ === 'titre_identite';
|
|
||||||
const isRepetition = typeDeChamp.type_champ === 'repetition';
|
|
||||||
const canBeMandatory =
|
|
||||||
!isHeaderSection && !isExplication && !state.isAnnotation;
|
|
||||||
|
|
||||||
const [ref, inView] = useInView({
|
|
||||||
threshold: 0.6
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateHandlers = createUpdateHandlers(
|
|
||||||
dispatch,
|
|
||||||
typeDeChamp,
|
|
||||||
index,
|
|
||||||
state.prefix
|
|
||||||
);
|
|
||||||
|
|
||||||
const typeDeChampsTypesForRepetition = state.typeDeChampsTypes.filter(
|
|
||||||
([, type]) => !EXCLUDE_FROM_REPETITION.includes(type)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
data-index={index}
|
|
||||||
data-in-view={inView ? true : undefined}
|
|
||||||
data-repetition={isRepetition ? true : undefined}
|
|
||||||
className={`type-de-champ form flex column justify-start ${
|
|
||||||
isHeaderSection ? 'type-header-section' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex justify-start section head ${
|
|
||||||
!isHeaderSection ? 'hr' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<DragHandle />
|
|
||||||
<TypeDeChampTypesSelect
|
|
||||||
handler={updateHandlers.type_champ}
|
|
||||||
options={state.typeDeChampsTypes}
|
|
||||||
/>
|
|
||||||
<div className="flex justify-start delete">
|
|
||||||
<button
|
|
||||||
className="button small icon-only danger"
|
|
||||||
onClick={() => {
|
|
||||||
if (confirm('Êtes vous sûr de vouloir supprimer ce champ ?'))
|
|
||||||
dispatch({
|
|
||||||
type: 'removeTypeDeChamp',
|
|
||||||
params: { typeDeChamp },
|
|
||||||
done: (estimatedFillDuration) => {
|
|
||||||
dispatch({
|
|
||||||
type: 'refresh',
|
|
||||||
params: { estimatedFillDuration }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrashIcon className="icon-size" />
|
|
||||||
<span className="sr-only">Supprimer</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`flex justify-start section ${
|
|
||||||
isDropDown || isFile || isCarte ? 'hr' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex column justify-start">
|
|
||||||
<MoveButton
|
|
||||||
isEnabled={!isFirstItem}
|
|
||||||
icon="arrow-up"
|
|
||||||
title="Déplacer le champ vers le haut"
|
|
||||||
onClick={() =>
|
|
||||||
dispatch({
|
|
||||||
type: 'moveTypeDeChampUp',
|
|
||||||
params: { typeDeChamp }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<MoveButton
|
|
||||||
isEnabled={!isLastItem}
|
|
||||||
icon="arrow-down"
|
|
||||||
title="Déplacer le champ vers le bas"
|
|
||||||
onClick={() =>
|
|
||||||
dispatch({
|
|
||||||
type: 'moveTypeDeChampDown',
|
|
||||||
params: { typeDeChamp }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex column justify-start">
|
|
||||||
<LibelleInput handler={updateHandlers.libelle} isVisible={true} />
|
|
||||||
<MandatoryInput
|
|
||||||
handler={updateHandlers.mandatory}
|
|
||||||
isVisible={canBeMandatory}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-start">
|
|
||||||
<DescriptionInput
|
|
||||||
isVisible={!isHeaderSection && !isTitreIdentite}
|
|
||||||
handler={updateHandlers.description}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-start section shift-left">
|
|
||||||
<TypeDeChampDropDownOptions
|
|
||||||
isVisible={isDropDown}
|
|
||||||
handler={updateHandlers.drop_down_list_value}
|
|
||||||
/>
|
|
||||||
<TypeDeChampDropDownSecondary
|
|
||||||
isVisible={isLinkedDropDown}
|
|
||||||
libelleHandler={updateHandlers.drop_down_secondary_libelle}
|
|
||||||
descriptionHandler={updateHandlers.drop_down_secondary_description}
|
|
||||||
/>
|
|
||||||
<TypeDeChampDropDownOther
|
|
||||||
isVisible={isSimpleDropDown}
|
|
||||||
handler={updateHandlers.drop_down_other}
|
|
||||||
/>
|
|
||||||
<TypeDeChampPieceJustificative
|
|
||||||
isVisible={isFile}
|
|
||||||
isTitreIdentite={isTitreIdentite}
|
|
||||||
directUploadUrl={state.directUploadUrl}
|
|
||||||
filename={typeDeChamp.piece_justificative_template_filename}
|
|
||||||
handler={updateHandlers.piece_justificative_template}
|
|
||||||
url={typeDeChamp.piece_justificative_template_url}
|
|
||||||
/>
|
|
||||||
<TypeDeChampCarteOptions isVisible={isCarte}>
|
|
||||||
{Object.entries(OPTIONS_FIELDS).map(([field, label]) => (
|
|
||||||
<TypeDeChampCarteOption
|
|
||||||
key={field}
|
|
||||||
label={label}
|
|
||||||
handler={updateHandlers[field]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</TypeDeChampCarteOptions>
|
|
||||||
<TypeDeChampRepetitionOptions
|
|
||||||
isVisible={isRepetition}
|
|
||||||
state={{
|
|
||||||
...state,
|
|
||||||
typeDeChampsTypes: typeDeChampsTypesForRepetition,
|
|
||||||
prefix: `repetition-${index}`,
|
|
||||||
typeDeChamps: typeDeChamp.types_de_champ || []
|
|
||||||
}}
|
|
||||||
typeDeChamp={typeDeChamp}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const DragHandle = SortableHandle(() => (
|
|
||||||
<div
|
|
||||||
className="handle small icon-only icon move-handle"
|
|
||||||
title="Déplacer le champ vers le haut ou vers le bas"
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
type HandlerInputElement =
|
|
||||||
| HTMLInputElement
|
|
||||||
| HTMLTextAreaElement
|
|
||||||
| HTMLSelectElement;
|
|
||||||
|
|
||||||
function createUpdateHandler(
|
|
||||||
dispatch: Dispatch<Action>,
|
|
||||||
typeDeChamp: TypeDeChamp,
|
|
||||||
field: keyof TypeDeChamp,
|
|
||||||
index: number,
|
|
||||||
prefix?: string
|
|
||||||
): Handler<HandlerInputElement> {
|
|
||||||
return {
|
|
||||||
id: `${prefix ? `${prefix}-` : ''}champ-${index}-${field}`,
|
|
||||||
name: field,
|
|
||||||
value: getValue(typeDeChamp, field),
|
|
||||||
onChange: ({ target }) =>
|
|
||||||
dispatch({
|
|
||||||
type: 'updateTypeDeChamp',
|
|
||||||
params: {
|
|
||||||
typeDeChamp,
|
|
||||||
field,
|
|
||||||
value: readValue(target)
|
|
||||||
},
|
|
||||||
done: (estimatedFillDuration: number) => {
|
|
||||||
return dispatch({
|
|
||||||
type: 'refresh',
|
|
||||||
params: { estimatedFillDuration }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createUpdateHandlers(
|
|
||||||
dispatch: Dispatch<Action>,
|
|
||||||
typeDeChamp: TypeDeChamp,
|
|
||||||
index: number,
|
|
||||||
prefix?: string
|
|
||||||
) {
|
|
||||||
return FIELDS.reduce((handlers, field) => {
|
|
||||||
handlers[field] = createUpdateHandler(
|
|
||||||
dispatch,
|
|
||||||
typeDeChamp,
|
|
||||||
field as keyof TypeDeChamp,
|
|
||||||
index,
|
|
||||||
prefix
|
|
||||||
);
|
|
||||||
return handlers;
|
|
||||||
}, {} as Record<string, Handler<HandlerInputElement>>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const OPTIONS_FIELDS = {
|
|
||||||
'options.cadastres': 'Cadastres',
|
|
||||||
'options.unesco': 'UNESCO',
|
|
||||||
'options.arretes_protection': 'Arrêtés de protection',
|
|
||||||
'options.conservatoire_littoral': 'Conservatoire du Littoral',
|
|
||||||
'options.reserves_chasse_faune_sauvage':
|
|
||||||
'Réserves nationales de chasse et de faune sauvage',
|
|
||||||
'options.reserves_biologiques': 'Réserves biologiques',
|
|
||||||
'options.reserves_naturelles': 'Réserves naturelles',
|
|
||||||
'options.natura_2000': 'Natura 2000',
|
|
||||||
'options.zones_humides': 'Zones humides d’importance internationale',
|
|
||||||
'options.znieff': 'ZNIEFF'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const FIELDS = [
|
|
||||||
'description',
|
|
||||||
'drop_down_list_value',
|
|
||||||
'drop_down_other',
|
|
||||||
'libelle',
|
|
||||||
'mandatory',
|
|
||||||
'parent_id',
|
|
||||||
'piece_justificative_template',
|
|
||||||
'private',
|
|
||||||
'type_champ',
|
|
||||||
'drop_down_secondary_libelle',
|
|
||||||
'drop_down_secondary_description',
|
|
||||||
...Object.keys(OPTIONS_FIELDS)
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
function getValue(obj: TypeDeChamp, path: string) {
|
|
||||||
const [, optionsPath] = path.split('.');
|
|
||||||
if (optionsPath) {
|
|
||||||
return (obj.editable_options || {})[optionsPath];
|
|
||||||
}
|
|
||||||
return obj[path as keyof TypeDeChamp] as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readValue(input: HandlerInputElement) {
|
|
||||||
return input.type === 'checkbox' && 'checked' in input
|
|
||||||
? input.checked
|
|
||||||
: input.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EXCLUDE_FROM_REPETITION = [
|
|
||||||
'carte',
|
|
||||||
'dossier_link',
|
|
||||||
'repetition',
|
|
||||||
'siret'
|
|
||||||
];
|
|
|
@ -1,25 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { Handler } from '../types';
|
|
||||||
|
|
||||||
export function TypeDeChampCarteOption({
|
|
||||||
label,
|
|
||||||
handler
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
handler: Handler<HTMLInputElement>;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<label htmlFor={handler.id}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={handler.id}
|
|
||||||
name={handler.name}
|
|
||||||
checked={!!handler.value}
|
|
||||||
onChange={handler.onChange}
|
|
||||||
className="small-margin small"
|
|
||||||
/>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import React, { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export function TypeDeChampCarteOptions({
|
|
||||||
isVisible,
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
isVisible: boolean;
|
|
||||||
children: ReactNode;
|
|
||||||
}) {
|
|
||||||
if (isVisible) {
|
|
||||||
return (
|
|
||||||
<div className="cell">
|
|
||||||
<label>Utilisation de la cartographie</label>
|
|
||||||
<div className="carte-options">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { Handler } from '../types';
|
|
||||||
|
|
||||||
export function TypeDeChampDropDownOptions({
|
|
||||||
isVisible,
|
|
||||||
handler
|
|
||||||
}: {
|
|
||||||
isVisible: boolean;
|
|
||||||
handler: Handler<HTMLTextAreaElement>;
|
|
||||||
}) {
|
|
||||||
if (isVisible) {
|
|
||||||
return (
|
|
||||||
<div className="cell">
|
|
||||||
<label htmlFor={handler.id}>Liste déroulante</label>
|
|
||||||
<textarea
|
|
||||||
id={handler.id}
|
|
||||||
name={handler.name}
|
|
||||||
value={handler.value}
|
|
||||||
onChange={handler.onChange}
|
|
||||||
rows={3}
|
|
||||||
cols={40}
|
|
||||||
placeholder="Ecrire une valeur par ligne et --valeur-- pour un séparateur."
|
|
||||||
className="small-margin small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { Handler } from '../types';
|
|
||||||
|
|
||||||
export function TypeDeChampDropDownOther({
|
|
||||||
isVisible,
|
|
||||||
handler
|
|
||||||
}: {
|
|
||||||
isVisible: boolean;
|
|
||||||
handler: Handler<HTMLInputElement>;
|
|
||||||
}) {
|
|
||||||
if (isVisible) {
|
|
||||||
return (
|
|
||||||
<div className="cell">
|
|
||||||
<label htmlFor={handler.id}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={handler.id}
|
|
||||||
name={handler.name}
|
|
||||||
checked={!!handler.value}
|
|
||||||
onChange={handler.onChange}
|
|
||||||
className="small-margin small"
|
|
||||||
/>
|
|
||||||
Proposer une option 'autre' avec un texte libre
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { Handler } from '../types';
|
|
||||||
|
|
||||||
export function TypeDeChampDropDownSecondary({
|
|
||||||
isVisible,
|
|
||||||
libelleHandler,
|
|
||||||
descriptionHandler
|
|
||||||
}: {
|
|
||||||
isVisible: boolean;
|
|
||||||
libelleHandler: Handler<HTMLInputElement>;
|
|
||||||
descriptionHandler: Handler<HTMLTextAreaElement>;
|
|
||||||
}) {
|
|
||||||
if (isVisible) {
|
|
||||||
return (
|
|
||||||
<div className="cell">
|
|
||||||
<label htmlFor={libelleHandler.id}>Libellé secondaire</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id={libelleHandler.id}
|
|
||||||
name={libelleHandler.name}
|
|
||||||
value={libelleHandler.value ?? ''}
|
|
||||||
onChange={libelleHandler.onChange}
|
|
||||||
className="small-margin small"
|
|
||||||
/>
|
|
||||||
<label htmlFor={descriptionHandler.id}>Description secondaire</label>
|
|
||||||
<textarea
|
|
||||||
id={descriptionHandler.id}
|
|
||||||
name={descriptionHandler.name}
|
|
||||||
value={descriptionHandler.value ?? ''}
|
|
||||||
onChange={descriptionHandler.onChange}
|
|
||||||
rows={3}
|
|
||||||
cols={40}
|
|
||||||
className="small-margin small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,98 +0,0 @@
|
||||||
import React, { ChangeEvent } from 'react';
|
|
||||||
|
|
||||||
import Uploader from '../../../shared/activestorage/uploader';
|
|
||||||
import type { Handler } from '../types';
|
|
||||||
|
|
||||||
export function TypeDeChampPieceJustificative({
|
|
||||||
isVisible,
|
|
||||||
isTitreIdentite,
|
|
||||||
url,
|
|
||||||
filename,
|
|
||||||
handler,
|
|
||||||
directUploadUrl
|
|
||||||
}: {
|
|
||||||
isVisible: boolean;
|
|
||||||
isTitreIdentite: boolean;
|
|
||||||
url?: string;
|
|
||||||
filename?: string;
|
|
||||||
handler: Handler<HTMLInputElement>;
|
|
||||||
directUploadUrl: string;
|
|
||||||
}) {
|
|
||||||
if (isVisible) {
|
|
||||||
const hasFile = !!filename;
|
|
||||||
return (
|
|
||||||
<div className="cell">
|
|
||||||
<label htmlFor={handler.id}>Modèle</label>
|
|
||||||
<FileInformation isVisible={hasFile} url={url} filename={filename} />
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id={handler.id}
|
|
||||||
name={handler.name}
|
|
||||||
onChange={onFileChange(handler, directUploadUrl)}
|
|
||||||
className="small-margin small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTitreIdentite) {
|
|
||||||
return (
|
|
||||||
<div className="cell">
|
|
||||||
<p id={`${handler.id}-description`}>
|
|
||||||
Dans le cadre de la RGPD, le titre d'identité sera supprimé lors
|
|
||||||
de l'acceptation du dossier
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FileInformation({
|
|
||||||
isVisible,
|
|
||||||
url,
|
|
||||||
filename
|
|
||||||
}: {
|
|
||||||
isVisible: boolean;
|
|
||||||
url?: string;
|
|
||||||
filename?: string;
|
|
||||||
}) {
|
|
||||||
if (isVisible) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<a href={url} rel="noopener noreferrer" target="_blank">
|
|
||||||
{filename}
|
|
||||||
</a>
|
|
||||||
<br /> Modifier :
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFileChange(
|
|
||||||
handler: Handler<HTMLInputElement>,
|
|
||||||
directUploadUrl: string
|
|
||||||
): (event: ChangeEvent<HTMLInputElement>) => void {
|
|
||||||
return async ({ target }) => {
|
|
||||||
const file = (target.files ?? [])[0];
|
|
||||||
if (file) {
|
|
||||||
const signedId = await uploadFile(target, file, directUploadUrl);
|
|
||||||
handler.onChange({
|
|
||||||
target: { value: signedId }
|
|
||||||
} as ChangeEvent<HTMLInputElement>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadFile(
|
|
||||||
input: HTMLInputElement,
|
|
||||||
file: File,
|
|
||||||
directUploadUrl: string
|
|
||||||
) {
|
|
||||||
const controller = new Uploader(input, file, directUploadUrl);
|
|
||||||
return controller.start().then((signedId) => {
|
|
||||||
input.value = '';
|
|
||||||
return signedId;
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
import React, { useReducer } from 'react';
|
|
||||||
import { PlusIcon } from '@heroicons/react/outline';
|
|
||||||
|
|
||||||
import { SortableContainer, addChampLabel } from '../utils';
|
|
||||||
import { TypeDeChampComponent } from './TypeDeChamp';
|
|
||||||
import typeDeChampsReducer from '../typeDeChampsReducer';
|
|
||||||
import type { State, TypeDeChamp } from '../types';
|
|
||||||
|
|
||||||
export function TypeDeChampRepetitionOptions({
|
|
||||||
isVisible,
|
|
||||||
state: parentState,
|
|
||||||
typeDeChamp
|
|
||||||
}: {
|
|
||||||
isVisible: boolean;
|
|
||||||
state: State;
|
|
||||||
typeDeChamp: TypeDeChamp;
|
|
||||||
}) {
|
|
||||||
const [state, dispatch] = useReducer(typeDeChampsReducer, parentState);
|
|
||||||
|
|
||||||
if (isVisible) {
|
|
||||||
return (
|
|
||||||
<div className="repetition flex-grow cell">
|
|
||||||
<SortableContainer
|
|
||||||
onSortEnd={(params) =>
|
|
||||||
dispatch({ type: 'onSortTypeDeChamps', params })
|
|
||||||
}
|
|
||||||
useDragHandle
|
|
||||||
>
|
|
||||||
{state.typeDeChamps.map((typeDeChamp, index) => (
|
|
||||||
<TypeDeChampComponent
|
|
||||||
dispatch={dispatch}
|
|
||||||
idx={index}
|
|
||||||
index={index}
|
|
||||||
isFirstItem={index === 0}
|
|
||||||
isLastItem={index === state.typeDeChamps.length - 1}
|
|
||||||
key={`champ-${typeDeChamp.id}`}
|
|
||||||
state={state}
|
|
||||||
typeDeChamp={typeDeChamp}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SortableContainer>
|
|
||||||
<button
|
|
||||||
className="button"
|
|
||||||
onClick={() =>
|
|
||||||
dispatch({
|
|
||||||
type: 'addNewRepetitionTypeDeChamp',
|
|
||||||
params: { typeDeChamp },
|
|
||||||
done: (estimatedFillDuration: number) =>
|
|
||||||
dispatch({ type: 'refresh', params: { estimatedFillDuration } })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PlusIcon className="icon-size" />
|
|
||||||
|
|
||||||
{addChampLabel(state.isAnnotation)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { Handler } from '../types';
|
|
||||||
|
|
||||||
export function TypeDeChampTypesSelect({
|
|
||||||
handler,
|
|
||||||
options
|
|
||||||
}: {
|
|
||||||
handler: Handler<HTMLSelectElement>;
|
|
||||||
options: [label: string, type: string][];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="cell">
|
|
||||||
<select
|
|
||||||
id={handler.id}
|
|
||||||
name={handler.name}
|
|
||||||
onChange={handler.onChange}
|
|
||||||
value={handler.value}
|
|
||||||
className="small-margin small inline"
|
|
||||||
>
|
|
||||||
{options.map(([label, key]) => (
|
|
||||||
<option key={key} value={key}>
|
|
||||||
{label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,98 +0,0 @@
|
||||||
import React, { useReducer } from 'react';
|
|
||||||
import { PlusIcon, ArrowCircleDownIcon } from '@heroicons/react/outline';
|
|
||||||
|
|
||||||
import { SortableContainer, addChampLabel } from '../utils';
|
|
||||||
import { TypeDeChampComponent } from './TypeDeChamp';
|
|
||||||
import typeDeChampsReducer from '../typeDeChampsReducer';
|
|
||||||
import type { TypeDeChamp, State } from '../types';
|
|
||||||
|
|
||||||
type TypeDeChampsProps = {
|
|
||||||
state: State;
|
|
||||||
typeDeChamps: TypeDeChamp[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function TypeDeChamps({
|
|
||||||
state: rootState,
|
|
||||||
typeDeChamps
|
|
||||||
}: TypeDeChampsProps) {
|
|
||||||
const [state, dispatch] = useReducer(typeDeChampsReducer, {
|
|
||||||
...rootState,
|
|
||||||
typeDeChamps
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasUnsavedChamps = state.typeDeChamps.some(
|
|
||||||
(tdc) => tdc.id == undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const formattedEstimatedFillDuration = state.estimatedFillDuration
|
|
||||||
? Math.max(1, Math.round(state.estimatedFillDuration / 60)) + ' mn'
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="champs-editor">
|
|
||||||
<SortableContainer
|
|
||||||
onSortEnd={(params) => dispatch({ type: 'onSortTypeDeChamps', params })}
|
|
||||||
lockAxis="y"
|
|
||||||
useDragHandle
|
|
||||||
>
|
|
||||||
{state.typeDeChamps.map((typeDeChamp, index) => (
|
|
||||||
<TypeDeChampComponent
|
|
||||||
dispatch={dispatch}
|
|
||||||
idx={index}
|
|
||||||
index={index}
|
|
||||||
isFirstItem={index === 0}
|
|
||||||
isLastItem={index === state.typeDeChamps.length - 1}
|
|
||||||
key={`champ-${typeDeChamp.id}`}
|
|
||||||
state={state}
|
|
||||||
typeDeChamp={typeDeChamp}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SortableContainer>
|
|
||||||
{state.typeDeChamps.length === 0 && (
|
|
||||||
<h2>
|
|
||||||
<ArrowCircleDownIcon className="icon-size" />
|
|
||||||
Cliquez sur le bouton «
|
|
||||||
{addChampLabel(state.isAnnotation)} » pour créer votre premier
|
|
||||||
champ.
|
|
||||||
</h2>
|
|
||||||
)}
|
|
||||||
<div className="footer"> </div>
|
|
||||||
<div className="buttons">
|
|
||||||
<button
|
|
||||||
className="button"
|
|
||||||
disabled={hasUnsavedChamps}
|
|
||||||
onClick={() =>
|
|
||||||
dispatch({
|
|
||||||
type: 'addNewTypeDeChamp',
|
|
||||||
done: (estimatedFillDuration: number) => {
|
|
||||||
dispatch({
|
|
||||||
type: 'refresh',
|
|
||||||
params: { estimatedFillDuration }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PlusIcon className="icon-size" />
|
|
||||||
|
|
||||||
{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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Flash } from './Flash';
|
|
||||||
import { OperationsQueue } from './OperationsQueue';
|
|
||||||
import { TypeDeChamps } from './components/TypeDeChamps';
|
|
||||||
import { TypeDeChamp } from './types';
|
|
||||||
|
|
||||||
type TypesDeChampEditorProps = {
|
|
||||||
baseUrl: string;
|
|
||||||
continuerUrl: string;
|
|
||||||
directUploadUrl: string;
|
|
||||||
isAnnotation: boolean;
|
|
||||||
typeDeChamps: TypeDeChamp[];
|
|
||||||
typeDeChampsTypes: [label: string, type: string][];
|
|
||||||
estimatedFillDuration: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type State = Omit<TypesDeChampEditorProps, 'baseUrl'> & {
|
|
||||||
flash: Flash;
|
|
||||||
queue: OperationsQueue;
|
|
||||||
defaultTypeDeChampAttributes: Pick<
|
|
||||||
TypeDeChamp,
|
|
||||||
| 'type_champ'
|
|
||||||
| 'types_de_champ'
|
|
||||||
| 'libelle'
|
|
||||||
| 'private'
|
|
||||||
| 'parent_id'
|
|
||||||
| 'mandatory'
|
|
||||||
>;
|
|
||||||
prefix?: string;
|
|
||||||
estimatedFillDuration?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TypesDeChampEditor(props: TypesDeChampEditorProps) {
|
|
||||||
const defaultTypeDeChampAttributes: Omit<TypeDeChamp, 'id'> = {
|
|
||||||
type_champ: 'text',
|
|
||||||
types_de_champ: [],
|
|
||||||
mandatory: false,
|
|
||||||
private: props.isAnnotation,
|
|
||||||
libelle: `${props.isAnnotation ? 'Nouvelle annotation' : 'Nouveau champ'} ${
|
|
||||||
props.typeDeChampsTypes[0][0]
|
|
||||||
}`
|
|
||||||
};
|
|
||||||
const state: State = {
|
|
||||||
flash: new Flash(props.isAnnotation),
|
|
||||||
queue: new OperationsQueue(props.baseUrl),
|
|
||||||
defaultTypeDeChampAttributes,
|
|
||||||
typeDeChamps: [],
|
|
||||||
typeDeChampsTypes: props.typeDeChampsTypes,
|
|
||||||
directUploadUrl: props.directUploadUrl,
|
|
||||||
isAnnotation: props.isAnnotation,
|
|
||||||
continuerUrl: props.continuerUrl,
|
|
||||||
estimatedFillDuration: props.estimatedFillDuration
|
|
||||||
};
|
|
||||||
|
|
||||||
return <TypeDeChamps state={state} typeDeChamps={props.typeDeChamps} />;
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
import type { TypeDeChamp, OperationsQueue } from './types';
|
|
||||||
|
|
||||||
export function createTypeDeChampOperation(
|
|
||||||
typeDeChamp: Omit<TypeDeChamp, 'id'>,
|
|
||||||
queue: OperationsQueue
|
|
||||||
) {
|
|
||||||
return queue
|
|
||||||
.enqueue({
|
|
||||||
path: '',
|
|
||||||
method: 'post',
|
|
||||||
payload: { type_de_champ: typeDeChamp }
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
handleResponseData(typeDeChamp, data as ResponseData);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function destroyTypeDeChampOperation(
|
|
||||||
typeDeChamp: TypeDeChamp,
|
|
||||||
queue: OperationsQueue
|
|
||||||
) {
|
|
||||||
return queue.enqueue({
|
|
||||||
path: `/${typeDeChamp.id}`,
|
|
||||||
method: 'delete',
|
|
||||||
payload: {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function moveTypeDeChampOperation(
|
|
||||||
typeDeChamp: TypeDeChamp,
|
|
||||||
index: number,
|
|
||||||
queue: OperationsQueue
|
|
||||||
) {
|
|
||||||
return queue.enqueue({
|
|
||||||
path: `/${typeDeChamp.id}/move`,
|
|
||||||
method: 'patch',
|
|
||||||
payload: { position: index }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTypeDeChampOperation(
|
|
||||||
typeDeChamp: TypeDeChamp,
|
|
||||||
queue: OperationsQueue
|
|
||||||
) {
|
|
||||||
return queue
|
|
||||||
.enqueue({
|
|
||||||
path: `/${typeDeChamp.id}`,
|
|
||||||
method: 'patch',
|
|
||||||
payload: { type_de_champ: typeDeChamp }
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
handleResponseData(typeDeChamp, data as ResponseData);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
export function estimateFillDuration(queue: OperationsQueue): Promise<number> {
|
|
||||||
return queue
|
|
||||||
.enqueue({
|
|
||||||
path: `/estimate_fill_duration`,
|
|
||||||
method: 'get'
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
const responseData = data as EstimatedFillDurationResponseData;
|
|
||||||
return responseData.estimated_fill_duration;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResponseData = { type_de_champ: Record<string, string> };
|
|
||||||
type EstimatedFillDurationResponseData = { estimated_fill_duration: number };
|
|
||||||
|
|
||||||
function handleResponseData(
|
|
||||||
typeDeChamp: Partial<TypeDeChamp>,
|
|
||||||
{ type_de_champ }: ResponseData
|
|
||||||
) {
|
|
||||||
for (const field of RESPONSE_FIELDS) {
|
|
||||||
typeDeChamp[field] = type_de_champ[field];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const RESPONSE_FIELDS = [
|
|
||||||
'id',
|
|
||||||
'piece_justificative_template_filename',
|
|
||||||
'piece_justificative_template_url'
|
|
||||||
] as const;
|
|
|
@ -1,360 +0,0 @@
|
||||||
import { debounce } from '@utils';
|
|
||||||
import {
|
|
||||||
createTypeDeChampOperation,
|
|
||||||
destroyTypeDeChampOperation,
|
|
||||||
moveTypeDeChampOperation,
|
|
||||||
updateTypeDeChampOperation,
|
|
||||||
estimateFillDuration
|
|
||||||
} from './operations';
|
|
||||||
import type { TypeDeChamp, State, Flash, OperationsQueue } from './types';
|
|
||||||
|
|
||||||
type AddNewTypeDeChampAction = {
|
|
||||||
type: 'addNewTypeDeChamp';
|
|
||||||
done: (estimatedFillDuration: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AddNewRepetitionTypeDeChampAction = {
|
|
||||||
type: 'addNewRepetitionTypeDeChamp';
|
|
||||||
params: { typeDeChamp: TypeDeChamp };
|
|
||||||
done: (estimatedFillDuration: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UpdateTypeDeChampAction = {
|
|
||||||
type: 'updateTypeDeChamp';
|
|
||||||
params: {
|
|
||||||
typeDeChamp: TypeDeChamp;
|
|
||||||
field: keyof TypeDeChamp;
|
|
||||||
value: string | boolean;
|
|
||||||
};
|
|
||||||
done: (estimatedFillDuration: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RemoveTypeDeChampAction = {
|
|
||||||
type: 'removeTypeDeChamp';
|
|
||||||
params: { typeDeChamp: TypeDeChamp };
|
|
||||||
done: (estimatedFillDuration: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MoveTypeDeChampUpAction = {
|
|
||||||
type: 'moveTypeDeChampUp';
|
|
||||||
params: { typeDeChamp: TypeDeChamp };
|
|
||||||
};
|
|
||||||
|
|
||||||
type MoveTypeDeChampDownAction = {
|
|
||||||
type: 'moveTypeDeChampDown';
|
|
||||||
params: { typeDeChamp: TypeDeChamp };
|
|
||||||
};
|
|
||||||
|
|
||||||
type OnSortTypeDeChampsAction = {
|
|
||||||
type: 'onSortTypeDeChamps';
|
|
||||||
params: { oldIndex: number; newIndex: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
type RefreshAction = {
|
|
||||||
type: 'refresh';
|
|
||||||
params: { estimatedFillDuration: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Action =
|
|
||||||
| AddNewTypeDeChampAction
|
|
||||||
| AddNewRepetitionTypeDeChampAction
|
|
||||||
| UpdateTypeDeChampAction
|
|
||||||
| RemoveTypeDeChampAction
|
|
||||||
| MoveTypeDeChampUpAction
|
|
||||||
| MoveTypeDeChampDownAction
|
|
||||||
| OnSortTypeDeChampsAction
|
|
||||||
| RefreshAction;
|
|
||||||
|
|
||||||
export default function typeDeChampsReducer(
|
|
||||||
state: State,
|
|
||||||
action: Action
|
|
||||||
): State {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'addNewTypeDeChamp':
|
|
||||||
return addNewTypeDeChamp(state, state.typeDeChamps, action.done);
|
|
||||||
case 'addNewRepetitionTypeDeChamp':
|
|
||||||
return addNewRepetitionTypeDeChamp(
|
|
||||||
state,
|
|
||||||
state.typeDeChamps,
|
|
||||||
action.params,
|
|
||||||
action.done
|
|
||||||
);
|
|
||||||
case 'updateTypeDeChamp':
|
|
||||||
return updateTypeDeChamp(
|
|
||||||
state,
|
|
||||||
state.typeDeChamps,
|
|
||||||
action.params,
|
|
||||||
action.done
|
|
||||||
);
|
|
||||||
case 'removeTypeDeChamp':
|
|
||||||
return removeTypeDeChamp(
|
|
||||||
state,
|
|
||||||
state.typeDeChamps,
|
|
||||||
action.params,
|
|
||||||
action.done
|
|
||||||
);
|
|
||||||
case 'moveTypeDeChampUp':
|
|
||||||
return moveTypeDeChampUp(state, state.typeDeChamps, action.params);
|
|
||||||
case 'moveTypeDeChampDown':
|
|
||||||
return moveTypeDeChampDown(state, state.typeDeChamps, action.params);
|
|
||||||
case 'onSortTypeDeChamps':
|
|
||||||
return onSortTypeDeChamps(state, state.typeDeChamps, action.params);
|
|
||||||
case 'refresh':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
typeDeChamps: [...state.typeDeChamps],
|
|
||||||
estimatedFillDuration: action.params.estimatedFillDuration
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTypeDeChamp(
|
|
||||||
state: State,
|
|
||||||
typeDeChamps: TypeDeChamp[],
|
|
||||||
insertAfter: { index: number; target: HTMLDivElement } | null,
|
|
||||||
done: (estimatedFillDuration: number) => void
|
|
||||||
) {
|
|
||||||
const typeDeChamp = {
|
|
||||||
...state.defaultTypeDeChampAttributes
|
|
||||||
};
|
|
||||||
|
|
||||||
createTypeDeChampOperation(typeDeChamp, state.queue)
|
|
||||||
.then(async () => {
|
|
||||||
if (insertAfter) {
|
|
||||||
// Move the champ to the correct position server-side
|
|
||||||
await moveTypeDeChampOperation(
|
|
||||||
typeDeChamp as TypeDeChamp,
|
|
||||||
insertAfter.index,
|
|
||||||
state.queue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
state.flash.success();
|
|
||||||
const estimatedFillDuration = await estimateFillDuration(state.queue);
|
|
||||||
done(estimatedFillDuration);
|
|
||||||
if (insertAfter) {
|
|
||||||
insertAfter.target.nextElementSibling?.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'start',
|
|
||||||
inline: 'nearest'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((message) => state.flash.error(message));
|
|
||||||
|
|
||||||
let newTypeDeChamps: TypeDeChamp[] = [
|
|
||||||
...typeDeChamps,
|
|
||||||
typeDeChamp as TypeDeChamp
|
|
||||||
];
|
|
||||||
if (insertAfter) {
|
|
||||||
// Move the champ to the correct position client-side
|
|
||||||
newTypeDeChamps = arrayMove(
|
|
||||||
newTypeDeChamps,
|
|
||||||
typeDeChamps.length,
|
|
||||||
insertAfter.index
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
typeDeChamps: newTypeDeChamps
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function addNewTypeDeChamp(
|
|
||||||
state: State,
|
|
||||||
typeDeChamps: TypeDeChamp[],
|
|
||||||
done: (estimatedFillDuration: number) => void
|
|
||||||
) {
|
|
||||||
return addTypeDeChamp(state, typeDeChamps, findItemToInsertAfter(), done);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addNewRepetitionTypeDeChamp(
|
|
||||||
state: State,
|
|
||||||
typeDeChamps: TypeDeChamp[],
|
|
||||||
{ typeDeChamp }: AddNewRepetitionTypeDeChampAction['params'],
|
|
||||||
done: (estimatedFillDuration: number) => void
|
|
||||||
) {
|
|
||||||
return addTypeDeChamp(
|
|
||||||
{
|
|
||||||
...state,
|
|
||||||
defaultTypeDeChampAttributes: {
|
|
||||||
...state.defaultTypeDeChampAttributes,
|
|
||||||
parent_id: typeDeChamp.id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
typeDeChamps,
|
|
||||||
null,
|
|
||||||
done
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTypeDeChamp(
|
|
||||||
state: State,
|
|
||||||
typeDeChamps: TypeDeChamp[],
|
|
||||||
{ typeDeChamp, field, value }: UpdateTypeDeChampAction['params'],
|
|
||||||
done: (estimatedFillDuration: number) => void
|
|
||||||
) {
|
|
||||||
if (field == 'type_champ' && !typeDeChamp.drop_down_list_value) {
|
|
||||||
switch (value) {
|
|
||||||
case 'linked_drop_down_list':
|
|
||||||
typeDeChamp.drop_down_list_value =
|
|
||||||
'--Fromage--\nbleu de sassenage\npicodon\n--Dessert--\néclair\ntarte aux pommes\n';
|
|
||||||
break;
|
|
||||||
case 'drop_down_list':
|
|
||||||
case 'multiple_drop_down_list':
|
|
||||||
typeDeChamp.drop_down_list_value = 'Premier choix\nDeuxième choix';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.startsWith('options.')) {
|
|
||||||
const [, optionsField] = field.split('.');
|
|
||||||
typeDeChamp.editable_options = typeDeChamp.editable_options || {};
|
|
||||||
typeDeChamp.editable_options[optionsField] = value as string;
|
|
||||||
} else {
|
|
||||||
Object.assign(typeDeChamp, { [field]: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
getUpdateHandler(typeDeChamp, state)(done);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
typeDeChamps: [...typeDeChamps]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTypeDeChamp(
|
|
||||||
state: State,
|
|
||||||
typeDeChamps: TypeDeChamp[],
|
|
||||||
{ typeDeChamp }: RemoveTypeDeChampAction['params'],
|
|
||||||
done: (estimatedFillDuration: number) => void
|
|
||||||
) {
|
|
||||||
destroyTypeDeChampOperation(typeDeChamp, state.queue)
|
|
||||||
.then(() => {
|
|
||||||
state.flash.success();
|
|
||||||
return estimateFillDuration(state.queue);
|
|
||||||
})
|
|
||||||
.then((estimatedFillDuration: number) => {
|
|
||||||
done(estimatedFillDuration);
|
|
||||||
})
|
|
||||||
.catch((message) => state.flash.error(message));
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
typeDeChamps: arrayRemove(typeDeChamps, typeDeChamp)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveTypeDeChampUp(
|
|
||||||
state: State,
|
|
||||||
typeDeChamps: TypeDeChamp[],
|
|
||||||
{ typeDeChamp }: MoveTypeDeChampUpAction['params']
|
|
||||||
) {
|
|
||||||
const oldIndex = typeDeChamps.indexOf(typeDeChamp);
|
|
||||||
const newIndex = oldIndex - 1;
|
|
||||||
|
|
||||||
moveTypeDeChampOperation(typeDeChamp, newIndex, state.queue)
|
|
||||||
.then(() => state.flash.success())
|
|
||||||
.catch((message) => state.flash.error(message));
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
typeDeChamps: arrayMove(typeDeChamps, oldIndex, newIndex)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveTypeDeChampDown(
|
|
||||||
state: State,
|
|
||||||
typeDeChamps: TypeDeChamp[],
|
|
||||||
{ typeDeChamp }: MoveTypeDeChampDownAction['params']
|
|
||||||
) {
|
|
||||||
const oldIndex = typeDeChamps.indexOf(typeDeChamp);
|
|
||||||
const newIndex = oldIndex + 1;
|
|
||||||
|
|
||||||
moveTypeDeChampOperation(typeDeChamp, newIndex, state.queue)
|
|
||||||
.then(() => state.flash.success())
|
|
||||||
.catch((message) => state.flash.error(message));
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
typeDeChamps: arrayMove(typeDeChamps, oldIndex, newIndex)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSortTypeDeChamps(
|
|
||||||
state: State,
|
|
||||||
typeDeChamps: TypeDeChamp[],
|
|
||||||
{ oldIndex, newIndex }: OnSortTypeDeChampsAction['params']
|
|
||||||
) {
|
|
||||||
moveTypeDeChampOperation(typeDeChamps[oldIndex], newIndex, state.queue)
|
|
||||||
.then(() => state.flash.success())
|
|
||||||
.catch((message) => state.flash.error(message));
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
typeDeChamps: arrayMove(typeDeChamps, oldIndex, newIndex)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function arrayRemove<T>(array: T[], item: T) {
|
|
||||||
array = Array.from(array);
|
|
||||||
array.splice(array.indexOf(item), 1);
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
function arrayMove<T>(array: T[], from: number, to: number) {
|
|
||||||
array = Array.from(array);
|
|
||||||
array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]);
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateHandlers = new WeakMap();
|
|
||||||
function getUpdateHandler(
|
|
||||||
typeDeChamp: TypeDeChamp,
|
|
||||||
{ queue, flash }: { queue: OperationsQueue; flash: Flash }
|
|
||||||
) {
|
|
||||||
let handler = updateHandlers.get(typeDeChamp);
|
|
||||||
if (!handler) {
|
|
||||||
handler = debounce(
|
|
||||||
(done: (estimatedFillDuration: number) => void) =>
|
|
||||||
updateTypeDeChampOperation(typeDeChamp, queue)
|
|
||||||
.then(() => {
|
|
||||||
flash.success();
|
|
||||||
return estimateFillDuration(queue);
|
|
||||||
})
|
|
||||||
.then((estimatedFillDuration: number) => {
|
|
||||||
done(estimatedFillDuration);
|
|
||||||
})
|
|
||||||
.catch((message) => flash.error(message)),
|
|
||||||
200
|
|
||||||
);
|
|
||||||
updateHandlers.set(typeDeChamp, handler);
|
|
||||||
}
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findItemToInsertAfter() {
|
|
||||||
const target = getLastVisibleTypeDeChamp();
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
return {
|
|
||||||
target,
|
|
||||||
index: parseInt(target.dataset.index ?? '0') + 1
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastVisibleTypeDeChamp() {
|
|
||||||
const typeDeChamps =
|
|
||||||
document.querySelectorAll<HTMLDivElement>('[data-in-view]');
|
|
||||||
const target = typeDeChamps[typeDeChamps.length - 1];
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
const parentTarget = target.closest<HTMLDivElement>('[data-repetition]');
|
|
||||||
if (parentTarget) {
|
|
||||||
return parentTarget;
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import type { ChangeEventHandler } from 'react';
|
|
||||||
|
|
||||||
export type { Flash } from './Flash';
|
|
||||||
export type { OperationsQueue } from './OperationsQueue';
|
|
||||||
export type { State } from '.';
|
|
||||||
export { Action } from './typeDeChampsReducer';
|
|
||||||
|
|
||||||
export type TypeDeChamp = {
|
|
||||||
id: string;
|
|
||||||
libelle: string;
|
|
||||||
type_champ: string;
|
|
||||||
private: boolean;
|
|
||||||
mandatory: boolean;
|
|
||||||
types_de_champ: TypeDeChamp[];
|
|
||||||
parent_id?: string;
|
|
||||||
piece_justificative_template_filename?: string;
|
|
||||||
piece_justificative_template_url?: string;
|
|
||||||
drop_down_list_value?: string;
|
|
||||||
editable_options?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Handler<Element extends HTMLElement> = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
onChange: ChangeEventHandler<Element>;
|
|
||||||
};
|
|
|
@ -1,16 +0,0 @@
|
||||||
import React, { ReactNode } from 'react';
|
|
||||||
import { SortableContainer as SortableContainerWrapper } from 'react-sortable-hoc';
|
|
||||||
|
|
||||||
export const SortableContainer = SortableContainerWrapper(
|
|
||||||
({ children }: { children: ReactNode }) => {
|
|
||||||
return <ul>{children}</ul>;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export function addChampLabel(isAnnotation: boolean) {
|
|
||||||
if (isAnnotation) {
|
|
||||||
return 'Ajouter une annotation';
|
|
||||||
} else {
|
|
||||||
return 'Ajouter un champ';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -56,8 +56,7 @@ registerComponents({
|
||||||
ComboRegionsSearch: () => import('../components/ComboRegionsSearch'),
|
ComboRegionsSearch: () => import('../components/ComboRegionsSearch'),
|
||||||
MapEditor: () => import('../components/MapEditor'),
|
MapEditor: () => import('../components/MapEditor'),
|
||||||
MapReader: () => import('../components/MapReader'),
|
MapReader: () => import('../components/MapReader'),
|
||||||
Trix: () => import('../components/Trix'),
|
Trix: () => import('../components/Trix')
|
||||||
TypesDeChampEditor: () => import('../components/TypesDeChampEditor')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is the global application namespace where we expose helpers used from rails views
|
// This is the global application namespace where we expose helpers used from rails views
|
||||||
|
|
|
@ -153,20 +153,6 @@ class ProcedureRevision < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def types_de_champ_public_as_json
|
|
||||||
types_de_champ = types_de_champ_public.includes(piece_justificative_template_attachment: :blob)
|
|
||||||
tdcs_as_json = types_de_champ.map(&:as_json_for_editor)
|
|
||||||
children_types_de_champ_as_json(tdcs_as_json, types_de_champ.filter(&:repetition?))
|
|
||||||
tdcs_as_json
|
|
||||||
end
|
|
||||||
|
|
||||||
def types_de_champ_private_as_json
|
|
||||||
types_de_champ = types_de_champ_private.includes(piece_justificative_template_attachment: :blob)
|
|
||||||
tdcs_as_json = types_de_champ.map(&:as_json_for_editor)
|
|
||||||
children_types_de_champ_as_json(tdcs_as_json, types_de_champ.filter(&:repetition?))
|
|
||||||
tdcs_as_json
|
|
||||||
end
|
|
||||||
|
|
||||||
# Estimated duration to fill the form, in seconds.
|
# Estimated duration to fill the form, in seconds.
|
||||||
#
|
#
|
||||||
# If the revision is locked (i.e. published), the result is cached (because type de champs can no longer be mutated).
|
# If the revision is locked (i.e. published), the result is cached (because type de champs can no longer be mutated).
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
class TypeDeChamp < ApplicationRecord
|
class TypeDeChamp < ApplicationRecord
|
||||||
self.ignored_columns = [:migrated_parent, :revision_id, :parent_id, :order_place]
|
self.ignored_columns = [:migrated_parent, :revision_id, :parent_id, :order_place]
|
||||||
|
|
||||||
|
FEATURE_FLAGS = {}
|
||||||
|
|
||||||
enum type_champs: {
|
enum type_champs: {
|
||||||
text: 'text',
|
text: 'text',
|
||||||
textarea: 'textarea',
|
textarea: 'textarea',
|
||||||
|
@ -299,63 +301,6 @@ class TypeDeChamp < ApplicationRecord
|
||||||
options.slice(*TypesDeChamp::CarteTypeDeChamp::LAYERS)
|
options.slice(*TypesDeChamp::CarteTypeDeChamp::LAYERS)
|
||||||
end
|
end
|
||||||
|
|
||||||
FEATURE_FLAGS = {}
|
|
||||||
|
|
||||||
def self.type_de_champ_types_for(procedure, user)
|
|
||||||
has_legacy_number = (procedure.types_de_champ + procedure.types_de_champ_private).any?(&:legacy_number?)
|
|
||||||
|
|
||||||
filter_featured_tdc = -> (tdc) do
|
|
||||||
feature_name = FEATURE_FLAGS[tdc]
|
|
||||||
feature_name.blank? || Flipper.enabled?(feature_name, user)
|
|
||||||
end
|
|
||||||
|
|
||||||
filter_tdc = -> (tdc) do
|
|
||||||
case tdc
|
|
||||||
when TypeDeChamp.type_champs.fetch(:number)
|
|
||||||
has_legacy_number
|
|
||||||
when TypeDeChamp.type_champs.fetch(:cnaf)
|
|
||||||
procedure.cnaf_enabled?
|
|
||||||
when TypeDeChamp.type_champs.fetch(:dgfip)
|
|
||||||
procedure.dgfip_enabled?
|
|
||||||
when TypeDeChamp.type_champs.fetch(:pole_emploi)
|
|
||||||
procedure.pole_emploi_enabled?
|
|
||||||
when TypeDeChamp.type_champs.fetch(:mesri)
|
|
||||||
procedure.mesri_enabled?
|
|
||||||
else
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
type_champs
|
|
||||||
.keys
|
|
||||||
.filter(&filter_tdc)
|
|
||||||
.filter(&filter_featured_tdc)
|
|
||||||
.map { |tdc| [I18n.t("activerecord.attributes.type_de_champ.type_champs.#{tdc}"), tdc] }
|
|
||||||
.sort_by(&:first)
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json_for_editor
|
|
||||||
as_json(
|
|
||||||
except: [
|
|
||||||
:created_at,
|
|
||||||
:options,
|
|
||||||
:private,
|
|
||||||
:stable_id,
|
|
||||||
:type,
|
|
||||||
:updated_at
|
|
||||||
],
|
|
||||||
methods: [
|
|
||||||
:drop_down_list_value,
|
|
||||||
:drop_down_other,
|
|
||||||
:drop_down_secondary_libelle,
|
|
||||||
:drop_down_secondary_description,
|
|
||||||
:piece_justificative_template_filename,
|
|
||||||
:piece_justificative_template_url,
|
|
||||||
:editable_options
|
|
||||||
]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_attribute_for_serialization(name)
|
def read_attribute_for_serialization(name)
|
||||||
if name == 'id'
|
if name == 'id'
|
||||||
stable_id
|
stable_id
|
||||||
|
|
|
@ -52,7 +52,7 @@ shared_examples 'type_de_champ_spec' do
|
||||||
before do
|
before do
|
||||||
allow(tdc).to receive(:piece_justificative_template).and_return(template_double)
|
allow(tdc).to receive(:piece_justificative_template).and_return(template_double)
|
||||||
|
|
||||||
tdc.update_attribute('type_champ', target_type_champ)
|
tdc.update(type_champ: target_type_champ)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the target type_champ is not pj' do
|
context 'when the target type_champ is not pj' do
|
||||||
|
@ -88,7 +88,7 @@ shared_examples 'type_de_champ_spec' do
|
||||||
let(:tdc) { create(:type_de_champ_repetition, :with_types_de_champ, procedure: procedure) }
|
let(:tdc) { create(:type_de_champ_repetition, :with_types_de_champ, procedure: procedure) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
tdc.update_attribute('type_champ', target_type_champ)
|
tdc.update(type_champ: target_type_champ)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the target type_champ is not repetition' do
|
context 'when the target type_champ is not repetition' do
|
||||||
|
@ -104,7 +104,7 @@ shared_examples 'type_de_champ_spec' do
|
||||||
let(:tdc) { create(:type_de_champ_drop_down_list) }
|
let(:tdc) { create(:type_de_champ_drop_down_list) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
tdc.update_attribute('type_champ', target_type_champ)
|
tdc.update(type_champ: target_type_champ)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the target type_champ is not drop_down_list' do
|
context 'when the target type_champ is not drop_down_list' do
|
||||||
|
@ -166,27 +166,6 @@ shared_examples 'type_de_champ_spec' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#type_de_champ_types_for' do
|
|
||||||
let(:procedure) { create(:procedure) }
|
|
||||||
let(:user) { create(:user) }
|
|
||||||
|
|
||||||
context 'when procedure without legacy "number"' do
|
|
||||||
it 'should have "nombre decimal" instead of "nombre"' do
|
|
||||||
expect(TypeDeChamp.type_de_champ_types_for(procedure, user).find { |tdc| tdc.last == TypeDeChamp.type_champs.fetch(:number) }).to be_nil
|
|
||||||
expect(TypeDeChamp.type_de_champ_types_for(procedure, user).find { |tdc| tdc.last == TypeDeChamp.type_champs.fetch(:decimal_number) }).not_to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when procedure with legacy "number"' do
|
|
||||||
let(:procedure) { create(:procedure, :with_number) }
|
|
||||||
|
|
||||||
it 'should have "nombre decimal" and "nombre"' do
|
|
||||||
expect(TypeDeChamp.type_de_champ_types_for(procedure, user).find { |tdc| tdc.last == TypeDeChamp.type_champs.fetch(:number) }).not_to be_nil
|
|
||||||
expect(TypeDeChamp.type_de_champ_types_for(procedure, user).find { |tdc| tdc.last == TypeDeChamp.type_champs.fetch(:decimal_number) }).not_to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#drop_down_list_options' do
|
describe '#drop_down_list_options' do
|
||||||
let(:value) do
|
let(:value) do
|
||||||
<<~EOS
|
<<~EOS
|
||||||
|
|
Loading…
Reference in a new issue