Revrite types de champ editor using React

This commit is contained in:
Paul Chavard 2019-03-20 16:29:33 +03:00
parent 5a032d344d
commit 47694d286e
22 changed files with 1141 additions and 48 deletions

View file

@ -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;
}
}

View file

@ -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 = `<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);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
function DescriptionInput({ isVisible, handler }) {
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;
}
DescriptionInput.propTypes = {
isVisible: PropTypes.bool,
handler: PropTypes.object
};
export default DescriptionInput;

View file

@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
function LibelleInput({ isVisible, handler }) {
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;
}
LibelleInput.propTypes = {
handler: PropTypes.object,
isVisible: PropTypes.bool
};
export default LibelleInput;

View file

@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
function MandatoryInput({ isVisible, handler }) {
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;
}
MandatoryInput.propTypes = {
handler: PropTypes.object,
isVisible: PropTypes.bool
};
export default MandatoryInput;

View file

@ -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 (
<button className="button small icon-only move" onClick={onClick}>
<FontAwesomeIcon icon={icon} />
</button>
);
}
return null;
}
MoveButton.propTypes = {
isVisible: PropTypes.bool,
icon: PropTypes.string,
onClick: PropTypes.func
};
export default MoveButton;

View file

@ -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 (
<div
ref={isLastItem ? state.lastTypeDeChampRef : null}
data-index={index}
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={() =>
dispatch({ type: 'removeTypeDeChamp', params: { typeDeChamp } })
}
>
<FontAwesomeIcon icon="trash" title="Supprimer" />
</button>
</div>
</div>
<div
className={`flex justify-start section ${
isDropDown || isFile || isCarte ? 'hr' : ''
}`}
>
<div className="flex column justify-start">
<MoveButton
isVisible={!isFirstItem}
icon="arrow-up"
onClick={() =>
dispatch({
type: 'moveTypeDeChampUp',
params: { typeDeChamp }
})
}
/>
<MoveButton
isVisible={!isLastItem}
icon="arrow-down"
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}
handler={updateHandlers.description}
/>
</div>
</div>
<div className="flex justify-start section shift-left">
<TypeDeChampDropDownOptions
isVisible={isDropDown}
handler={updateHandlers.drop_down_list_value}
/>
<TypeDeChampPieceJustificative
isVisible={isFile}
directUploadUrl={state.directUploadUrl}
filename={typeDeChamp.piece_justificative_template_filename}
handler={updateHandlers.piece_justificative_template}
url={typeDeChamp.piece_justificative_template_url}
/>
<TypeDeChampCarteOptions isVisible={isCarte}>
<TypeDeChampCarteOption
label="Quartiers prioritaires"
handler={updateHandlers.quartiers_prioritaires}
/>
<TypeDeChampCarteOption
label="Cadastres"
handler={updateHandlers.cadastres}
/>
<TypeDeChampCarteOption
label="Parcelles Agricoles"
handler={updateHandlers.parcelles_agricoles}
/>
</TypeDeChampCarteOptions>
<TypeDeChampRepetitionOptions
isVisible={isRepetition}
state={{
...state,
typeDeChampsTypes: typeDeChampsTypesForRepetition,
prefix: `repetition-${index}`,
typeDeChamps: typeDeChamp.types_de_champ || []
}}
typeDeChamp={typeDeChamp}
/>
</div>
</div>
);
}
);
TypeDeChamp.propTypes = {
dispatch: PropTypes.func,
idx: PropTypes.number,
isFirstItem: PropTypes.bool,
isLastItem: PropTypes.bool,
state: PropTypes.object,
typeDeChamp: PropTypes.object
};
const DragHandle = sortableHandle(() => (
<div className="handle button small icon-only">
<FontAwesomeIcon icon="arrows-alt-v" size="lg" />
</div>
));
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;

View file

@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
function TypeDeChampCarteOption({ label, handler }) {
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>
);
}
TypeDeChampCarteOption.propTypes = {
label: PropTypes.string,
handler: PropTypes.object
};
export default TypeDeChampCarteOption;

View file

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
function TypeDeChampCarteOptions({ isVisible, children }) {
if (isVisible) {
return (
<div className="cell">
<label>Utilisation de la cartographie</label>
<div className="carte-options">{children}</div>
</div>
);
}
return null;
}
TypeDeChampCarteOptions.propTypes = {
isVisible: PropTypes.bool,
children: PropTypes.array
};
export default TypeDeChampCarteOptions;

View file

@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
function TypeDeChampDropDownOptions({ isVisible, value, handler }) {
if (isVisible) {
return (
<div className="cell">
<label htmlFor={handler.id}>Liste déroulante</label>
<textarea
id={handler.id}
name={handler.name}
value={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;
}
TypeDeChampDropDownOptions.propTypes = {
isVisible: PropTypes.bool,
value: PropTypes.string,
handler: PropTypes.object
};
export default TypeDeChampDropDownOptions;

View file

@ -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 (
<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>
);
}
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 (
<>
<a href={url} rel="noopener noreferrer" target="_blank">
{filename}
</a>
<br /> 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;

View file

@ -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 (
<div className="repetition flex-grow cell">
<SortableContainer
onSortEnd={params => dispatch({ type: 'onSortTypeDeChamps', params })}
useDragHandle
>
{state.typeDeChamps.map((typeDeChamp, index) => (
<TypeDeChamp
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: () => dispatch({ type: 'refresh' })
})
}
>
{addChampLabel(state.isAnnotation)}
</button>
</div>
);
}
return null;
}
TypeDeChampRepetitionOptions.propTypes = {
isVisible: PropTypes.bool,
state: PropTypes.object,
typeDeChamp: PropTypes.object
};
export default TypeDeChampRepetitionOptions;

View file

@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
function TypeDeChampTypesSelect({ handler, options }) {
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>
);
}
TypeDeChampTypesSelect.propTypes = {
handler: PropTypes.object,
options: PropTypes.array
};
export default TypeDeChampTypesSelect;

View file

@ -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 (
<div className="champs-editor">
<SortableContainer
onSortEnd={params => dispatch({ type: 'onSortTypeDeChamps', params })}
lockAxis="y"
useDragHandle
>
{state.typeDeChamps.map((typeDeChamp, index) => (
<TypeDeChamp
dispatch={dispatch}
idx={index}
index={index}
isFirstItem={index === 0}
isLastItem={index === state.typeDeChamps.length - 1}
key={`champ-${typeDeChamp.id}`}
state={state}
typeDeChamp={typeDeChamp}
/>
))}
</SortableContainer>
<div className="footer">&nbsp;</div>
<div className="buttons">
<button
className="button"
onClick={() =>
dispatch({
type: 'addNewTypeDeChamp',
done: () => dispatch({ type: 'refresh' })
})
}
>
{addChampLabel(state.isAnnotation)}
</button>
<button
className="button primary"
onClick={() => state.flash.success()}
>
Enregistrer
</button>
</div>
</div>
);
}
TypeDeChamps.propTypes = {
state: PropTypes.object,
typeDeChamps: PropTypes.array
};
export default TypeDeChamps;

View file

@ -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 (
<TypeDeChamps state={this.state} typeDeChamps={this.props.typeDeChamps} />
);
}
}
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;

View file

@ -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'
];

View file

@ -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;
}

View file

@ -0,0 +1,14 @@
import React from 'react';
import { sortableContainer } from 'react-sortable-hoc';
export const SortableContainer = sortableContainer(({ children }) => {
return <ul>{children}</ul>;
});
export function addChampLabel(isAnnotation) {
if (isAnnotation) {
return 'Ajouter une annotation';
} else {
return 'Ajouter un champ';
}
}

View file

@ -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 {

View file

@ -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",

View file

@ -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"