diff --git a/app/assets/stylesheets/new_design/procedure_champs_editor.scss b/app/assets/stylesheets/new_design/procedure_champs_editor.scss
index 591eb48df..917b2d561 100644
--- a/app/assets/stylesheets/new_design/procedure_champs_editor.scss
+++ b/app/assets/stylesheets/new_design/procedure_champs_editor.scss
@@ -1,15 +1,8 @@
@import "colors";
@import "constants";
-#champs-editor {
- .spinner {
- margin-right: auto;
- margin-left: auto;
- margin-top: 80px;
- }
-}
-
-.draggable-item {
+.type-de-champ {
+ background-color: $white;
border: 1px solid $border-grey;
border-radius: 5px;
margin-bottom: 10px;
@@ -18,24 +11,11 @@
.handle {
cursor: ns-resize;
margin-right: 10px;
- margin-top: 8px;
}
- .error-message {
- text-align: center;
- flex-grow: 1;
- font-size: 14px;
- color: $light-grey;
- display: flex;
- align-items: center;
- flex-direction: column;
- justify-content: space-around;
-
- .content {
- background-color: $medium-red;
- border-radius: 8px;
- padding: 4px 10px;
- }
+ .move {
+ margin-right: 10px;
+ margin-bottom: 5px;
}
&.type-header-section {
@@ -46,12 +26,6 @@
}
}
- &:not(.type-header-section) {
- input.error {
- border: 1px solid $medium-red;
- }
- }
-
.flex {
&.section {
padding: 10px 10px 0 10px;
@@ -67,7 +41,7 @@
}
&.shift-left {
- margin-left: 35px;
+ margin-left: 55px;
}
&.head {
@@ -112,21 +86,23 @@
}
}
-.footer {
- margin-bottom: 70px;
-}
+.champs-editor {
+ .footer {
+ margin-bottom: 40px;
+ }
-.buttons {
- display: flex;
- justify-content: space-between;
- margin: 0px;
- position: fixed;
- bottom: 0px;
- background-color: $white;
- max-width: $page-width;
- width: 100%;
- border: 1px solid $border-grey;
- padding: 10px;
- border-top-left-radius: 5px;
- border-top-right-radius: 5px;
+ .buttons {
+ display: flex;
+ justify-content: space-between;
+ margin: 0px;
+ position: fixed;
+ bottom: 0px;
+ background-color: $white;
+ max-width: $page-width;
+ width: 100%;
+ border: 1px solid $border-grey;
+ padding: 10px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ }
}
diff --git a/app/javascript/components/TypesDeChampEditor/Flash.js b/app/javascript/components/TypesDeChampEditor/Flash.js
new file mode 100644
index 000000000..945c6efd8
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/Flash.js
@@ -0,0 +1,35 @@
+export default class Flash {
+ constructor(isAnnotation) {
+ this.element = document.querySelector('#flash_messages');
+ this.isAnnotation = isAnnotation;
+ }
+ success() {
+ if (this.isAnnotation) {
+ this.add('Annotations privées enregistrées.');
+ } else {
+ this.add('Formulaire enregistré.');
+ }
+ }
+ error(message) {
+ this.add(message, true);
+ }
+ clear() {
+ this.element.innerHTML = '';
+ }
+ add(message, isError) {
+ const html = `
`;
+
+ this.element.innerHTML = html;
+
+ clearTimeout(this.timeout);
+ this.timeout = setTimeout(() => {
+ this.clear();
+ }, 4000);
+ }
+}
diff --git a/app/javascript/components/TypesDeChampEditor/OperationsQueue.js b/app/javascript/components/TypesDeChampEditor/OperationsQueue.js
new file mode 100644
index 000000000..f849e64bc
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/OperationsQueue.js
@@ -0,0 +1,52 @@
+import { to, getJSON } from '@utils';
+
+export default class OperationsQueue {
+ constructor(baseUrl) {
+ this.queue = [];
+ this.isRunning = false;
+ this.baseUrl = baseUrl;
+ }
+
+ async run() {
+ if (this.queue.length > 0) {
+ this.isRunning = true;
+ const operation = this.queue.shift();
+ await this.exec(operation);
+ this.run();
+ } else {
+ this.isRunning = false;
+ }
+ }
+
+ enqueue(operation) {
+ return new Promise((resolve, reject) => {
+ this.queue.push({ ...operation, resolve, reject });
+ if (!this.isRunning) {
+ this.run();
+ }
+ });
+ }
+
+ async exec(operation) {
+ const { path, method, payload, resolve, reject } = operation;
+ const url = `${this.baseUrl}${path}`;
+ const [data, xhr] = await to(getJSON(url, payload, method));
+
+ if (xhr) {
+ handleError(xhr, reject);
+ } else {
+ resolve(data);
+ }
+ }
+}
+
+function handleError(xhr, reject) {
+ try {
+ const {
+ errors: [message]
+ } = JSON.parse(xhr.responseText);
+ reject(message);
+ } catch (e) {
+ reject(xhr.responseText);
+ }
+}
diff --git a/app/javascript/components/TypesDeChampEditor/components/DescriptionInput.js b/app/javascript/components/TypesDeChampEditor/components/DescriptionInput.js
new file mode 100644
index 000000000..2d17f752c
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/DescriptionInput.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function DescriptionInput({ isVisible, handler }) {
+ if (isVisible) {
+ return (
+
+
+
+
+ );
+ }
+ return null;
+}
+
+DescriptionInput.propTypes = {
+ isVisible: PropTypes.bool,
+ handler: PropTypes.object
+};
+
+export default DescriptionInput;
diff --git a/app/javascript/components/TypesDeChampEditor/components/LibelleInput.js b/app/javascript/components/TypesDeChampEditor/components/LibelleInput.js
new file mode 100644
index 000000000..f1ca7c07c
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/LibelleInput.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function LibelleInput({ isVisible, handler }) {
+ if (isVisible) {
+ return (
+
+
+
+
+ );
+ }
+ return null;
+}
+
+LibelleInput.propTypes = {
+ handler: PropTypes.object,
+ isVisible: PropTypes.bool
+};
+
+export default LibelleInput;
diff --git a/app/javascript/components/TypesDeChampEditor/components/MandatoryInput.js b/app/javascript/components/TypesDeChampEditor/components/MandatoryInput.js
new file mode 100644
index 000000000..436a23345
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/MandatoryInput.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function MandatoryInput({ isVisible, handler }) {
+ if (isVisible) {
+ return (
+
+
+
+
+ );
+ }
+ return null;
+}
+
+MandatoryInput.propTypes = {
+ handler: PropTypes.object,
+ isVisible: PropTypes.bool
+};
+
+export default MandatoryInput;
diff --git a/app/javascript/components/TypesDeChampEditor/components/MoveButton.js b/app/javascript/components/TypesDeChampEditor/components/MoveButton.js
new file mode 100644
index 000000000..3d70b034e
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/MoveButton.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+function MoveButton({ isVisible, icon, onClick }) {
+ if (isVisible) {
+ return (
+
+ );
+ }
+ return null;
+}
+
+MoveButton.propTypes = {
+ isVisible: PropTypes.bool,
+ icon: PropTypes.string,
+ onClick: PropTypes.func
+};
+
+export default MoveButton;
diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.js b/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.js
new file mode 100644
index 000000000..e9c5f7bd3
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.js
@@ -0,0 +1,227 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { sortableElement, sortableHandle } from 'react-sortable-hoc';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+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 TypeDeChampPieceJustificative from './TypeDeChampPieceJustificative';
+import TypeDeChampRepetitionOptions from './TypeDeChampRepetitionOptions';
+import TypeDeChampTypesSelect from './TypeDeChampTypesSelect';
+
+const TypeDeChamp = sortableElement(
+ ({ typeDeChamp, dispatch, idx: index, isFirstItem, isLastItem, state }) => {
+ const isDropDown = [
+ 'drop_down_list',
+ 'multiple_drop_down_list',
+ 'linked_drop_down_list'
+ ].includes(typeDeChamp.type_champ);
+ 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 isRepetition = typeDeChamp.type_champ === 'repetition';
+ const canBeMandatory =
+ !isHeaderSection && !isExplication && !state.isAnnotation;
+
+ const updateHandlers = createUpdateHandlers(
+ dispatch,
+ typeDeChamp,
+ index,
+ state.prefix
+ );
+
+ const typeDeChampsTypesForRepetition = state.typeDeChampsTypes.filter(
+ ([, type]) => !EXCLUDE_FROM_REPETITION.includes(type)
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ dispatch({
+ type: 'moveTypeDeChampUp',
+ params: { typeDeChamp }
+ })
+ }
+ />
+
+ dispatch({
+ type: 'moveTypeDeChampDown',
+ params: { typeDeChamp }
+ })
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+);
+
+TypeDeChamp.propTypes = {
+ dispatch: PropTypes.func,
+ idx: PropTypes.number,
+ isFirstItem: PropTypes.bool,
+ isLastItem: PropTypes.bool,
+ state: PropTypes.object,
+ typeDeChamp: PropTypes.object
+};
+
+const DragHandle = sortableHandle(() => (
+
+
+
+));
+
+function createUpdateHandler(dispatch, typeDeChamp, field, index, prefix) {
+ return {
+ id: `${prefix ? `${prefix}-` : ''}champ-${index}-${field}`,
+ name: field,
+ value: typeDeChamp[field],
+ onChange: ({ target }) =>
+ dispatch({
+ type: 'updateTypeDeChamp',
+ params: {
+ typeDeChamp,
+ field,
+ value: readValue(target)
+ },
+ done: () => dispatch({ type: 'refresh' })
+ })
+ };
+}
+
+function createUpdateHandlers(dispatch, typeDeChamp, index, prefix) {
+ return FIELDS.reduce((handlers, field) => {
+ handlers[field] = createUpdateHandler(
+ dispatch,
+ typeDeChamp,
+ field,
+ index,
+ prefix
+ );
+ return handlers;
+ }, {});
+}
+
+export const FIELDS = [
+ 'cadastres',
+ 'description',
+ 'drop_down_list_value',
+ 'libelle',
+ 'mandatory',
+ 'order_place',
+ 'parcelles_agricoles',
+ 'parent_id',
+ 'piece_justificative_template',
+ 'private',
+ 'quartiers_prioritaires',
+ 'type_champ'
+];
+
+function readValue(input) {
+ return input.type === 'checkbox' ? input.checked : input.value;
+}
+
+const EXCLUDE_FROM_REPETITION = [
+ 'carte',
+ 'dossier_link',
+ 'repetition',
+ 'siret'
+];
+
+export default TypeDeChamp;
diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChampCarteOption.js b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampCarteOption.js
new file mode 100644
index 000000000..8de18874f
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampCarteOption.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function TypeDeChampCarteOption({ label, handler }) {
+ return (
+
+ );
+}
+
+TypeDeChampCarteOption.propTypes = {
+ label: PropTypes.string,
+ handler: PropTypes.object
+};
+
+export default TypeDeChampCarteOption;
diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChampCarteOptions.js b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampCarteOptions.js
new file mode 100644
index 000000000..eac595de8
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampCarteOptions.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function TypeDeChampCarteOptions({ isVisible, children }) {
+ if (isVisible) {
+ return (
+
+
+
{children}
+
+ );
+ }
+ return null;
+}
+
+TypeDeChampCarteOptions.propTypes = {
+ isVisible: PropTypes.bool,
+ children: PropTypes.array
+};
+
+export default TypeDeChampCarteOptions;
diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChampDropDownOptions.js b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampDropDownOptions.js
new file mode 100644
index 000000000..61570058d
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampDropDownOptions.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function TypeDeChampDropDownOptions({ isVisible, value, handler }) {
+ if (isVisible) {
+ return (
+
+
+
+
+ );
+ }
+ return null;
+}
+
+TypeDeChampDropDownOptions.propTypes = {
+ isVisible: PropTypes.bool,
+ value: PropTypes.string,
+ handler: PropTypes.object
+};
+
+export default TypeDeChampDropDownOptions;
diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChampPieceJustificative.js b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampPieceJustificative.js
new file mode 100644
index 000000000..a766191b5
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampPieceJustificative.js
@@ -0,0 +1,81 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Uploader from '../../../shared/activestorage/uploader';
+
+function TypeDeChampPieceJustificative({
+ isVisible,
+ url,
+ filename,
+ handler,
+ directUploadUrl
+}) {
+ if (isVisible) {
+ const hasFile = !!filename;
+ return (
+
+
+
+
+
+ );
+ }
+ return null;
+}
+
+TypeDeChampPieceJustificative.propTypes = {
+ isVisible: PropTypes.bool,
+ url: PropTypes.string,
+ filename: PropTypes.string,
+ handler: PropTypes.object,
+ directUploadUrl: PropTypes.string
+};
+
+function FileInformation({ isVisible, url, filename }) {
+ if (isVisible) {
+ return (
+ <>
+
+ {filename}
+
+
Modifier :
+ >
+ );
+ }
+ return null;
+}
+
+FileInformation.propTypes = {
+ isVisible: PropTypes.bool,
+ url: PropTypes.string,
+ filename: PropTypes.string
+};
+
+function onFileChange(handler, directUploadUrl) {
+ return async ({ target }) => {
+ const file = target.files[0];
+ if (file) {
+ const signedId = await uploadFile(target, file, directUploadUrl);
+ handler.onChange({
+ target: {
+ value: signedId
+ }
+ });
+ }
+ };
+}
+
+function uploadFile(input, file, directUploadUrl) {
+ const controller = new Uploader(input, file, directUploadUrl);
+ return controller.start().then(signedId => {
+ input.value = null;
+ return signedId;
+ });
+}
+
+export default TypeDeChampPieceJustificative;
diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChampRepetitionOptions.js b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampRepetitionOptions.js
new file mode 100644
index 000000000..984057fdf
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampRepetitionOptions.js
@@ -0,0 +1,63 @@
+import React, { useReducer, useRef } from 'react';
+import PropTypes from 'prop-types';
+
+import { SortableContainer, addChampLabel } from '../utils';
+import TypeDeChamp from './TypeDeChamp';
+import typeDeChampsReducer from '../typeDeChampsReducer';
+
+function TypeDeChampRepetitionOptions({
+ isVisible,
+ state: parentState,
+ typeDeChamp
+}) {
+ const lastTypeDeChampRef = useRef(null);
+ const [state, dispatch] = useReducer(typeDeChampsReducer, {
+ ...parentState,
+ lastTypeDeChampRef
+ });
+
+ if (isVisible) {
+ return (
+
+ dispatch({ type: 'onSortTypeDeChamps', params })}
+ useDragHandle
+ >
+ {state.typeDeChamps.map((typeDeChamp, index) => (
+
+ ))}
+
+
+
+ );
+ }
+ return null;
+}
+
+TypeDeChampRepetitionOptions.propTypes = {
+ isVisible: PropTypes.bool,
+ state: PropTypes.object,
+ typeDeChamp: PropTypes.object
+};
+
+export default TypeDeChampRepetitionOptions;
diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChampTypesSelect.js b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampTypesSelect.js
new file mode 100644
index 000000000..1c53a220e
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampTypesSelect.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function TypeDeChampTypesSelect({ handler, options }) {
+ return (
+
+
+
+ );
+}
+
+TypeDeChampTypesSelect.propTypes = {
+ handler: PropTypes.object,
+ options: PropTypes.array
+};
+
+export default TypeDeChampTypesSelect;
diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChamps.js b/app/javascript/components/TypesDeChampEditor/components/TypeDeChamps.js
new file mode 100644
index 000000000..2523dbf9d
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChamps.js
@@ -0,0 +1,72 @@
+import React, { useReducer, useRef } from 'react';
+import PropTypes from 'prop-types';
+
+import { SortableContainer, addChampLabel } from '../utils';
+import TypeDeChamp from './TypeDeChamp';
+import typeDeChampsReducer from '../typeDeChampsReducer';
+
+function TypeDeChamps({ state: rootState, typeDeChamps }) {
+ const lastTypeDeChampRef = useRef(null);
+ const [state, dispatch] = useReducer(typeDeChampsReducer, {
+ ...rootState,
+ lastTypeDeChampRef,
+ typeDeChamps
+ });
+
+ if (state.typeDeChamps.length === 0) {
+ dispatch({
+ type: 'addFirstTypeDeChamp',
+ done: () => dispatch({ type: 'refresh' })
+ });
+ }
+
+ return (
+
+
dispatch({ type: 'onSortTypeDeChamps', params })}
+ lockAxis="y"
+ useDragHandle
+ >
+ {state.typeDeChamps.map((typeDeChamp, index) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
+
+TypeDeChamps.propTypes = {
+ state: PropTypes.object,
+ typeDeChamps: PropTypes.array
+};
+
+export default TypeDeChamps;
diff --git a/app/javascript/components/TypesDeChampEditor/index.js b/app/javascript/components/TypesDeChampEditor/index.js
new file mode 100644
index 000000000..07b550014
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/index.js
@@ -0,0 +1,63 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import {
+ faArrowDown,
+ faArrowsAltV,
+ faArrowUp,
+ faTrash
+} from '@fortawesome/free-solid-svg-icons';
+
+import Flash from './Flash';
+import OperationsQueue from './OperationsQueue';
+import TypeDeChamps from './components/TypeDeChamps';
+
+library.add(faArrowDown, faArrowsAltV, faArrowUp, faTrash);
+
+class TypesDeChampEditor extends Component {
+ constructor({
+ baseUrl,
+ typeDeChampsTypes,
+ directUploadUrl,
+ isAnnotation,
+ typeDeChamps
+ }) {
+ super({ typeDeChamps });
+ const defaultTypeDeChampAttributes = {
+ type_champ: 'text',
+ types_de_champ: [],
+ private: isAnnotation,
+ libelle: `${isAnnotation ? 'Nouvelle annotation' : 'Nouveau champ'} ${
+ typeDeChampsTypes[0][0]
+ }`
+ };
+ this.state = {
+ flash: new Flash(isAnnotation),
+ queue: new OperationsQueue(baseUrl),
+ defaultTypeDeChampAttributes,
+ typeDeChampsTypes,
+ directUploadUrl,
+ isAnnotation
+ };
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+TypesDeChampEditor.propTypes = {
+ baseUrl: PropTypes.string,
+ directUploadUrl: PropTypes.string,
+ isAnnotation: PropTypes.bool,
+ typeDeChamps: PropTypes.array,
+ typeDeChampsTypes: PropTypes.array
+};
+
+export function createReactUJSElement(props) {
+ return React.createElement(TypesDeChampEditor, props);
+}
+
+export default TypesDeChampEditor;
diff --git a/app/javascript/components/TypesDeChampEditor/operations.js b/app/javascript/components/TypesDeChampEditor/operations.js
new file mode 100644
index 000000000..aed8926a8
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/operations.js
@@ -0,0 +1,51 @@
+export function createTypeDeChampOperation(typeDeChamp, queue) {
+ return queue
+ .enqueue({
+ path: '',
+ method: 'post',
+ payload: { type_de_champ: typeDeChamp }
+ })
+ .then(data => {
+ handleResponseData(typeDeChamp, data);
+ });
+}
+
+export function destroyTypeDeChampOperation(typeDeChamp, queue) {
+ return queue.enqueue({
+ path: `/${typeDeChamp.id}`,
+ method: 'delete',
+ payload: {}
+ });
+}
+
+export function moveTypeDeChampOperation(typeDeChamp, index, queue) {
+ return queue.enqueue({
+ path: `/${typeDeChamp.id}/move`,
+ method: 'patch',
+ payload: { order_place: index }
+ });
+}
+
+export function updateTypeDeChampOperation(typeDeChamp, queue) {
+ return queue
+ .enqueue({
+ path: `/${typeDeChamp.id}`,
+ method: 'patch',
+ payload: { type_de_champ: typeDeChamp }
+ })
+ .then(data => {
+ handleResponseData(typeDeChamp, data);
+ });
+}
+
+function handleResponseData(typeDeChamp, { type_de_champ }) {
+ for (let field of RESPONSE_FIELDS) {
+ typeDeChamp[field] = type_de_champ[field];
+ }
+}
+
+const RESPONSE_FIELDS = [
+ 'id',
+ 'piece_justificative_template_filename',
+ 'piece_justificative_template_url'
+];
diff --git a/app/javascript/components/TypesDeChampEditor/typeDeChampsReducer.js b/app/javascript/components/TypesDeChampEditor/typeDeChampsReducer.js
new file mode 100644
index 000000000..e42d4a4d7
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/typeDeChampsReducer.js
@@ -0,0 +1,184 @@
+import scrollToComponent from 'react-scroll-to-component';
+import { debounce } from '@utils';
+import {
+ createTypeDeChampOperation,
+ destroyTypeDeChampOperation,
+ moveTypeDeChampOperation,
+ updateTypeDeChampOperation
+} from './operations';
+
+export default function typeDeChampsReducer(state, { type, params, done }) {
+ switch (type) {
+ case 'addNewTypeDeChamp':
+ return addNewTypeDeChamp(state, state.typeDeChamps, done);
+ case 'addFirstTypeDeChamp':
+ return addFirstTypeDeChamp(state, state.typeDeChamps, done);
+ case 'addNewRepetitionTypeDeChamp':
+ return addNewRepetitionTypeDeChamp(
+ state,
+ state.typeDeChamps,
+ params.typeDeChamp,
+ done
+ );
+ case 'updateTypeDeChamp':
+ return updateTypeDeChamp(state, state.typeDeChamps, params, done);
+ case 'removeTypeDeChamp':
+ return removeTypeDeChamp(state, state.typeDeChamps, params);
+ case 'moveTypeDeChampUp':
+ return moveTypeDeChampUp(state, state.typeDeChamps, params);
+ case 'moveTypeDeChampDown':
+ return moveTypeDeChampDown(state, state.typeDeChamps, params);
+ case 'onSortTypeDeChamps':
+ return onSortTypeDeChamps(state, state.typeDeChamps, params);
+ case 'refresh':
+ return { ...state, typeDeChamps: [...state.typeDeChamps] };
+ default:
+ throw new Error(`Unknown action "${type}"`);
+ }
+}
+
+function addNewTypeDeChamp(state, typeDeChamps, done) {
+ const typeDeChamp = {
+ ...state.defaultTypeDeChampAttributes,
+ order_place: typeDeChamps.length
+ };
+
+ createTypeDeChampOperation(typeDeChamp, state.queue)
+ .then(() => {
+ state.flash.success();
+ done();
+ if (state.lastTypeDeChampRef) {
+ scrollToComponent(state.lastTypeDeChampRef.current);
+ }
+ })
+ .catch(message => state.flash.error(message));
+
+ return {
+ ...state,
+ typeDeChamps: [...typeDeChamps, typeDeChamp]
+ };
+}
+
+function addNewRepetitionTypeDeChamp(state, typeDeChamps, typeDeChamp, done) {
+ return addNewTypeDeChamp(
+ {
+ ...state,
+ defaultTypeDeChampAttributes: {
+ ...state.defaultTypeDeChampAttributes,
+ parent_id: typeDeChamp.id
+ }
+ },
+ typeDeChamps,
+ done
+ );
+}
+
+function addFirstTypeDeChamp(state, typeDeChamps, done) {
+ const typeDeChamp = { ...state.defaultTypeDeChampAttributes, order_place: 0 };
+
+ createTypeDeChampOperation(typeDeChamp, state.queue)
+ .then(() => done())
+ .catch(message => state.flash.error(message));
+
+ return {
+ ...state,
+ typeDeChamps: [...typeDeChamps, typeDeChamp]
+ };
+}
+
+function updateTypeDeChamp(
+ state,
+ typeDeChamps,
+ { typeDeChamp, field, value },
+ done
+) {
+ typeDeChamp[field] = value;
+
+ getUpdateHandler(typeDeChamp, state)(done);
+
+ return {
+ ...state,
+ typeDeChamps: [...typeDeChamps]
+ };
+}
+
+function removeTypeDeChamp(state, typeDeChamps, { typeDeChamp }) {
+ destroyTypeDeChampOperation(typeDeChamp, state.queue)
+ .then(() => state.flash.success())
+ .catch(message => state.flash.error(message));
+
+ return {
+ ...state,
+ typeDeChamps: arrayRemove(typeDeChamps, typeDeChamp)
+ };
+}
+
+function moveTypeDeChampUp(state, typeDeChamps, { typeDeChamp }) {
+ 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, typeDeChamps, { typeDeChamp }) {
+ 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, typeDeChamps, { oldIndex, newIndex }) {
+ 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(array, item) {
+ array = Array.from(array);
+ array.splice(array.indexOf(item), 1);
+ return array;
+}
+
+function arrayMove(array, from, to) {
+ 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, { queue, flash }) {
+ let handler = updateHandlers.get(typeDeChamp);
+ if (!handler) {
+ handler = debounce(
+ done =>
+ updateTypeDeChampOperation(typeDeChamp, queue)
+ .then(() => {
+ flash.success();
+ done();
+ })
+ .catch(message => flash.error(message)),
+ 200
+ );
+ updateHandlers.set(typeDeChamp, handler);
+ }
+ return handler;
+}
diff --git a/app/javascript/components/TypesDeChampEditor/utils.js b/app/javascript/components/TypesDeChampEditor/utils.js
new file mode 100644
index 000000000..f2f5932ad
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/utils.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import { sortableContainer } from 'react-sortable-hoc';
+
+export const SortableContainer = sortableContainer(({ children }) => {
+ return ;
+});
+
+export function addChampLabel(isAnnotation) {
+ if (isAnnotation) {
+ return 'Ajouter une annotation';
+ } else {
+ return 'Ajouter un champ';
+ }
+}
diff --git a/app/javascript/shared/utils.js b/app/javascript/shared/utils.js
index 9b413e98b..cb1e62c9d 100644
--- a/app/javascript/shared/utils.js
+++ b/app/javascript/shared/utils.js
@@ -51,6 +51,10 @@ export function on(selector, eventName, fn) {
);
}
+export function to(promise) {
+ return promise.then(result => [result]).catch(error => [null, error]);
+}
+
function offset(element) {
const rect = element.getBoundingClientRect();
return {
diff --git a/package.json b/package.json
index 2963c8048..d4a6f0016 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"ramda": "^0.25.0",
"react_ujs": "^2.4.4",
"react-dom": "^16.8.4",
+ "react-scroll-to-component": "^1.0.2",
"react-sortable-hoc": "^1.7.1",
"react": "^16.8.4",
"select2": "^4.0.6-rc.1",
diff --git a/yarn.lock b/yarn.lock
index 25610d6dc..11923e3e5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2235,11 +2235,48 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
+component-clone@0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/component-clone/-/component-clone-0.2.2.tgz#c7f5979822880fad8cfb0962ba29186d061ee04f"
+ integrity sha1-x/WXmCKID62M+wliuikYbQYe4E8=
+ dependencies:
+ component-type "*"
+
+component-emitter@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.0.tgz#ccd113a86388d06482d03de3fc7df98526ba8efe"
+ integrity sha1-zNETqGOI0GSC0D3j/H35hSa6jv4=
+
component-emitter@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
+component-raf@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/component-raf/-/component-raf-1.2.0.tgz#b2bc72d43f1b014fde7a4b3c447c764bc73ccbaa"
+ integrity sha1-srxy1D8bAU/eeks8RHx2S8c8y6o=
+
+component-tween@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/component-tween/-/component-tween-1.2.0.tgz#cc39ce5dbab05b52825f41d1947638a0b01b2b8a"
+ integrity sha1-zDnOXbqwW1KCX0HRlHY4oLAbK4o=
+ dependencies:
+ component-clone "0.2.2"
+ component-emitter "1.2.0"
+ component-type "1.1.0"
+ ease-component "1.0.0"
+
+component-type@*:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-type/-/component-type-1.2.1.tgz#8a47901700238e4fc32269771230226f24b415a9"
+ integrity sha1-ikeQFwAjjk/DIml3EjAibyS0Fak=
+
+component-type@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/component-type/-/component-type-1.1.0.tgz#95b666aad53e5c8d1f2be135c45b5d499197c0c5"
+ integrity sha1-lbZmqtU+XI0fK+E1xFtdSZGXwMU=
+
compressible@~2.0.13:
version "2.0.14"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.14.tgz#326c5f507fbb055f54116782b969a81b67a29da7"
@@ -2971,6 +3008,11 @@ duplexify@^3.4.2, duplexify@^3.6.0:
readable-stream "^2.0.0"
stream-shift "^1.0.0"
+ease-component@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ease-component/-/ease-component-1.0.0.tgz#b375726db0b5b04595b77440396fec7daa5d77c9"
+ integrity sha1-s3VybbC1sEWVt3RAOW/sfapdd8k=
+
ecc-jsbn@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
@@ -7236,6 +7278,13 @@ react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2"
integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA==
+react-scroll-to-component@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/react-scroll-to-component/-/react-scroll-to-component-1.0.2.tgz#f260dc936c62a53e772786d7832fe0884e195354"
+ integrity sha1-8mDck2xipT53J4bXgy/giE4ZU1Q=
+ dependencies:
+ scroll-to "0.0.2"
+
react-sortable-hoc@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-1.7.1.tgz#2d61c6003f22523ab3adfe37e69774c5d73c1ac6"
@@ -7700,6 +7749,14 @@ schema-utils@^1.0.0:
ajv-errors "^1.0.0"
ajv-keywords "^3.1.0"
+scroll-to@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/scroll-to/-/scroll-to-0.0.2.tgz#936d398a9133660a2492145c2c0081dfcb0728f3"
+ integrity sha1-k205ipEzZgokkhRcLACB38sHKPM=
+ dependencies:
+ component-raf "1.2.0"
+ component-tween "1.2.0"
+
scss-tokenizer@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"