[Types de Champ Editeur] Save on change and only edited model

This commit is contained in:
Paul Chavard 2019-02-06 18:19:27 +01:00
parent dea78e2e4e
commit 5da5f75c5f
14 changed files with 351 additions and 254 deletions

View file

@ -1,49 +1,13 @@
module NewAdministrateur module NewAdministrateur
class ProceduresController < AdministrateurController class ProceduresController < AdministrateurController
before_action :retrieve_procedure, only: [:champs, :annotations, :update] before_action :retrieve_procedure, only: [:champs, :annotations]
before_action :procedure_locked?, only: [:champs, :annotations, :update] before_action :procedure_locked?, only: [:champs, :annotations]
TYPE_DE_CHAMP_ATTRIBUTES_BASE = [
:_destroy,
:libelle,
:description,
:order_place,
:type_champ,
:id,
:mandatory,
:piece_justificative_template,
:quartiers_prioritaires,
:cadastres,
:parcelles_agricoles,
drop_down_list_attributes: [:value]
]
TYPE_DE_CHAMP_ATTRIBUTES = TYPE_DE_CHAMP_ATTRIBUTES_BASE.dup
TYPE_DE_CHAMP_ATTRIBUTES << {
types_de_champ_attributes: TYPE_DE_CHAMP_ATTRIBUTES_BASE
}
def apercu def apercu
@dossier = procedure_without_control.new_dossier @dossier = procedure_without_control.new_dossier
@tab = apercu_tab @tab = apercu_tab
end end
def update
if @procedure.update(procedure_params)
flash.now.notice = if params[:procedure][:types_de_champ_attributes].present?
'Formulaire mis à jour.'
elsif params[:procedure][:types_de_champ_private_attributes].present?
'Annotations privées mises à jour.'
else
'Démarche enregistrée.'
end
reset_procedure
else
flash.now.alert = @procedure.errors.full_messages
end
end
private private
def apercu_tab def apercu_tab
@ -53,12 +17,5 @@ module NewAdministrateur
def procedure_without_control def procedure_without_control
Procedure.find(params[:id]) Procedure.find(params[:id])
end end
def procedure_params
params.required(:procedure).permit(
types_de_champ_attributes: TYPE_DE_CHAMP_ATTRIBUTES,
types_de_champ_private_attributes: TYPE_DE_CHAMP_ATTRIBUTES
)
end
end end
end end

View file

@ -0,0 +1,76 @@
module NewAdministrateur
class TypesDeChampController < AdministrateurController
before_action :retrieve_procedure, only: [:create, :update, :destroy]
before_action :procedure_locked?, only: [:create, :update, :destroy]
def create
type_de_champ = TypeDeChamp.new(type_de_champ_create_params)
if type_de_champ.save
reset_procedure
render json: serialize_type_de_champ(type_de_champ), status: :created
else
render json: { errors: type_de_champ.errors.full_messages }, status: :unprocessable_entity
end
end
def update
type_de_champ = TypeDeChamp.where(procedure: @procedure).find(params[:id])
if type_de_champ.update(type_de_champ_update_params)
reset_procedure
render json: serialize_type_de_champ(type_de_champ)
else
render json: { errors: type_de_champ.errors.full_messages }, status: :unprocessable_entity
end
end
def destroy
type_de_champ = TypeDeChamp.where(procedure: @procedure).find(params[:id])
type_de_champ.destroy!
reset_procedure
head :no_content
end
private
def serialize_type_de_champ(type_de_champ)
{
type_de_champ: type_de_champ.as_json(
except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private],
methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value]
)
}
end
def type_de_champ_create_params
params.required(:type_de_champ).permit(:libelle,
:description,
:order_place,
:type_champ,
:private,
:parent_id,
:mandatory,
:piece_justificative_template,
:quartiers_prioritaires,
:cadastres,
:parcelles_agricoles,
:drop_down_list_value).merge(procedure: @procedure)
end
def type_de_champ_update_params
params.required(:type_de_champ).permit(:libelle,
:description,
:order_place,
:type_champ,
:mandatory,
:piece_justificative_template,
:quartiers_prioritaires,
:cadastres,
:parcelles_agricoles,
:drop_down_list_value)
end
end
end

View file

@ -39,7 +39,8 @@ module ProcedureHelper
type: "champ", type: "champ",
types_de_champ_options: types_de_champ_options.to_json, types_de_champ_options: types_de_champ_options.to_json,
types_de_champ: types_de_champ_as_json(procedure.types_de_champ).to_json, types_de_champ: types_de_champ_as_json(procedure.types_de_champ).to_json,
direct_uploads_url: rails_direct_uploads_url, save_url: procedure_types_de_champ_path(procedure),
direct_upload_url: rails_direct_uploads_url,
drag_icon_url: image_url("icons/drag.svg") drag_icon_url: image_url("icons/drag.svg")
} }
end end
@ -49,18 +50,12 @@ module ProcedureHelper
type: "annotation", type: "annotation",
types_de_champ_options: types_de_champ_options.to_json, types_de_champ_options: types_de_champ_options.to_json,
types_de_champ: types_de_champ_as_json(procedure.types_de_champ_private).to_json, types_de_champ: types_de_champ_as_json(procedure.types_de_champ_private).to_json,
direct_uploads_url: rails_direct_uploads_url, save_url: procedure_types_de_champ_path(procedure),
direct_upload_url: rails_direct_uploads_url,
drag_icon_url: image_url("icons/drag.svg") drag_icon_url: image_url("icons/drag.svg")
} }
end end
def procedure_data(procedure)
{
types_de_champ: types_de_champ_as_json(procedure.types_de_champ),
types_de_champ_private: types_de_champ_as_json(procedure.types_de_champ_private)
}.to_json
end
private private
TOGGLES = { TOGGLES = {
@ -79,14 +74,12 @@ module ProcedureHelper
types_de_champ types_de_champ
end end
TYPES_DE_CHAMP_INCLUDE = { drop_down_list: { only: :value } }
TYPES_DE_CHAMP_BASE = { TYPES_DE_CHAMP_BASE = {
except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private], except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private],
methods: [:piece_justificative_template_filename, :piece_justificative_template_url], methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value]
include: TYPES_DE_CHAMP_INCLUDE
} }
TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE
.merge(include: TYPES_DE_CHAMP_INCLUDE.merge(types_de_champ: TYPES_DE_CHAMP_BASE)) .merge(include: { types_de_champ: TYPES_DE_CHAMP_BASE })
def types_de_champ_as_json(types_de_champ) def types_de_champ_as_json(types_de_champ)
types_de_champ.includes(:drop_down_list, types_de_champ.includes(:drop_down_list,

View file

@ -1,40 +1,23 @@
import { getJSON, debounce } from '@utils';
import { DirectUpload } from 'activestorage';
export default { export default {
props: ['state', 'update', 'index', 'item'], props: ['state', 'index', 'item'],
computed: { computed: {
isDirty() { isValid() {
return (
this.state.version &&
this.state.unsavedInvalidItems.size > 0 &&
this.state.unsavedItems.has(this.itemId)
);
},
isInvalid() {
if (this.deleted) { if (this.deleted) {
return false; return true;
} }
if (this.libelle) { if (this.libelle) {
return !this.libelle.trim(); return !!this.libelle.trim();
} }
return true; return false;
},
itemId() {
return this.item.id || this.clientId;
},
changeLog() {
return [this.itemId, !this.isInvalid];
}, },
itemClassName() { itemClassName() {
const classNames = [`draggable-item-${this.index}`]; const classNames = [`draggable-item-${this.index}`];
if (this.isHeaderSection) { if (this.isHeaderSection) {
classNames.push('type-header-section'); classNames.push('type-header-section');
} }
if (this.isDirty) {
if (this.isInvalid) {
classNames.push('invalid');
} else {
classNames.push('dirty');
}
}
return classNames.join(' '); return classNames.join(' ');
}, },
isDropDown() { isDropDown() {
@ -73,6 +56,47 @@ export default {
return 'types_de_champ_attributes'; return 'types_de_champ_attributes';
} }
}, },
payload() {
const payload = {
libelle: this.libelle,
type_champ: this.typeChamp,
mandatory: this.mandatory,
description: this.description,
drop_down_list_value: this.dropDownListValue,
order_place: this.index
};
if (this.pieceJustificativeTemplate) {
payload.piece_justificative_template = this.pieceJustificativeTemplate;
}
if (this.state.parentId) {
payload.parent_id = this.state.parentId;
}
if (!this.id && this.state.isAnnotation) {
payload.private = true;
}
Object.assign(payload, this.options);
return payload;
},
saveUrl() {
if (this.id) {
return `${this.state.saveUrl}/${this.id}`;
}
return this.state.saveUrl;
},
savePayload() {
if (this.deleted) {
return {};
}
return { type_de_champ: this.payload };
},
saveMethod() {
if (this.deleted) {
return 'delete';
} else if (this.id) {
return 'patch';
}
return 'post';
},
typesDeChamp() { typesDeChamp() {
return this.item.types_de_champ; return this.item.types_de_champ;
}, },
@ -85,39 +109,46 @@ export default {
return Object.assign({}, this.state, { return Object.assign({}, this.state, {
typesDeChamp: this.typesDeChamp, typesDeChamp: this.typesDeChamp,
typesDeChampOptions: this.typesDeChampOptions, typesDeChampOptions: this.typesDeChampOptions,
prefix: `${this.state.prefix}[${this.attribute}][${this.index}]` prefix: `${this.state.prefix}[${this.attribute}][${this.index}]`,
parentId: this.id
}); });
} }
}, },
data() { data() {
return { return {
id: this.item.id,
typeChamp: this.item.type_champ, typeChamp: this.item.type_champ,
libelle: this.item.libelle, libelle: this.item.libelle,
mandatory: this.item.mandatory, mandatory: this.item.mandatory,
description: this.item.description, description: this.item.description,
dropDownList: this.item.drop_down_list && this.item.drop_down_list.value, pieceJustificativeTemplate: null,
pieceJustificativeTemplateUrl: this.item.piece_justificative_template_url,
pieceJustificativeTemplateFilename: this.item
.piece_justificative_template_filename,
dropDownListValue: this.item.drop_down_list_value,
deleted: false, deleted: false,
clientId: `id-${clientIds++}` isSaving: false,
isUploading: false,
hasChanges: false
}; };
}, },
created() { watch: {
for (let path of PATHS_TO_WATCH) { index() {
this.$watch(path, () => this.update(this.changeLog)); this.update();
} }
}, },
mounted() { created() {
if (this.isInvalid) { this.debouncedSave = debounce(() => this.save(), 500);
this.update(this.changeLog, false); this.debouncedUpload = debounce(evt => this.upload(evt), 500);
}
}, },
methods: { methods: {
removeChamp(item) { removeChamp() {
if (item.id) { if (this.id) {
this.deleted = true; this.deleted = true;
this.debouncedSave();
} else { } else {
const index = this.state.typesDeChamp.indexOf(item); const index = this.state.typesDeChamp.indexOf(this.item);
this.state.typesDeChamp.splice(index, 1); this.state.typesDeChamp.splice(index, 1);
this.update([this.itemId, true]);
} }
}, },
nameFor(name) { nameFor(name) {
@ -132,6 +163,76 @@ export default {
type_champ: 'text', type_champ: 'text',
types_de_champ: [] types_de_champ: []
}); });
},
update() {
this.hasChanges = true;
if (this.isValid) {
if (this.state.inFlight === 0) {
this.state.flash.clear();
}
this.debouncedSave();
}
},
upload(evt) {
if (this.isUploading) {
this.debouncedUpload();
} else {
const input = evt.target;
const file = input.files[0];
if (file) {
this.isUploading = true;
uploadFile(this.state.directUploadUrl, file).then(({ signed_id }) => {
this.pieceJustificativeTemplate = signed_id;
this.isUploading = false;
this.debouncedSave();
});
}
input.value = null;
}
},
save() {
if (this.isSaving) {
this.debouncedSave();
} else {
this.isSaving = true;
this.state.inFlight++;
getJSON(this.saveUrl, this.savePayload, this.saveMethod)
.then(data => {
this.onSuccess(data);
})
.catch(xhr => {
this.onError(xhr);
});
}
},
onSuccess(data) {
if (data && data.type_de_champ) {
this.id = data.type_de_champ.id;
this.pieceJustificativeTemplateUrl =
data.type_de_champ.piece_justificative_template_url;
this.pieceJustificativeTemplateFilename =
data.type_de_champ.piece_justificative_template_filename;
this.pieceJustificativeTemplate = null;
}
this.state.inFlight--;
this.isSaving = false;
this.hasChanges = false;
if (this.state.inFlight === 0) {
this.state.flash.success();
}
},
onError(xhr) {
this.isSaving = false;
this.state.inFlight--;
try {
const {
errors: [message]
} = JSON.parse(xhr.responseText);
this.state.flash.error(message);
} catch (e) {
this.state.flash.error(xhr.responseText);
}
} }
} }
}; };
@ -143,21 +244,20 @@ const EXCLUDE_FROM_REPETITION = [
'siret' 'siret'
]; ];
const PATHS_TO_WATCH = [
'typeChamp',
'libelle',
'mandatory',
'description',
'dropDownList',
'options.quartiers_prioritaires',
'options.cadastres',
'options.parcelles_agricoles',
'index',
'deleted'
];
function castBoolean(value) { function castBoolean(value) {
return value && value != 0; return value && value != 0;
} }
let clientIds = 0; function uploadFile(directUploadUrl, file) {
const upload = new DirectUpload(file, directUploadUrl);
return new Promise((resolve, reject) => {
upload.create((error, blob) => {
if (error) {
reject(error);
} else {
resolve(blob);
}
});
});
}

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="deleted" v-if="deleted"> <div class="deleted" v-if="deleted">
<input type="hidden" :name="nameFor('id')" :value="item.id"> <input type="hidden" :name="nameFor('id')" :value="id">
<input type="hidden" :name="nameFor('_destroy')" value="true"> <input type="hidden" :name="nameFor('_destroy')" value="true">
</div> </div>
@ -14,6 +14,7 @@
:id="elementIdFor('type_champ')" :id="elementIdFor('type_champ')"
:name="nameFor('type_champ')" :name="nameFor('type_champ')"
v-model="typeChamp" v-model="typeChamp"
@change="update"
class="small-margin small inline"> class="small-margin small inline">
<option v-for="option in state.typesDeChampOptions" :key="option[1]" :value="option[1]"> <option v-for="option in state.typesDeChampOptions" :key="option[1]" :value="option[1]">
{{ option[0] }} {{ option[0] }}
@ -21,21 +22,7 @@
</select> </select>
</div> </div>
<div class="flex justify-start delete"> <div class="flex justify-start delete">
<div v-if="isDirty" class="error-message"> <button class="button danger" @click.prevent="removeChamp">
<span v-if="isInvalid" class="content">
Le libellé doit être rempli.
</span>
<span v-else class="content">
<template v-if="state.isAnnotation">
Modifications non sauvegardées. Le libellé doit être rempli sur tous les annotations.
</template>
<template v-else>
Modifications non sauvegardées. Le libellé doit être rempli sur tous les champs.
</template>
</span>
</div>
<button class="button danger" @click.prevent="removeChamp(item)">
Supprimer Supprimer
</button> </button>
</div> </div>
@ -51,8 +38,9 @@
:id="elementIdFor('libelle')" :id="elementIdFor('libelle')"
:name="nameFor('libelle')" :name="nameFor('libelle')"
v-model="libelle" v-model="libelle"
@change="update"
class="small-margin small" class="small-margin small"
:class="{ error: isDirty && isInvalid }"> :class="{ error: hasChanges && !isValid }">
</div> </div>
<div class="cell" v-show="!isHeaderSection && !isExplication && !state.isAnnotation"> <div class="cell" v-show="!isHeaderSection && !isExplication && !state.isAnnotation">
@ -65,6 +53,7 @@
:id="elementIdFor('mandatory')" :id="elementIdFor('mandatory')"
:name="nameFor('mandatory')" :name="nameFor('mandatory')"
v-model="mandatory" v-model="mandatory"
@change="update"
class="small-margin small" class="small-margin small"
value="1"> value="1">
</div> </div>
@ -78,6 +67,7 @@
:id="elementIdFor('description')" :id="elementIdFor('description')"
:name="nameFor('description')" :name="nameFor('description')"
v-model="description" v-model="description"
@change="update"
rows=3 rows=3
cols=40 cols=40
class="small-margin small"> class="small-margin small">
@ -93,7 +83,8 @@
<textarea <textarea
:id="elementIdFor('drop_down_list')" :id="elementIdFor('drop_down_list')"
:name="nameFor('drop_down_list_attributes[value]')" :name="nameFor('drop_down_list_attributes[value]')"
v-model="dropDownList" v-model="dropDownListValue"
@change="update"
rows=3 rows=3
cols=40 cols=40
placeholder="Ecrire une valeur par ligne et --valeur-- pour un séparateur." placeholder="Ecrire une valeur par ligne et --valeur-- pour un séparateur."
@ -104,9 +95,9 @@
<label :for="elementIdFor('piece_justificative_template')"> <label :for="elementIdFor('piece_justificative_template')">
Modèle Modèle
</label> </label>
<template v-if="item.piece_justificative_template_url"> <template v-if="pieceJustificativeTemplateUrl">
<a :href="item.piece_justificative_template_url" target="_blank"> <a :href="pieceJustificativeTemplateUrl" target="_blank">
{{item.piece_justificative_template_filename}} {{pieceJustificativeTemplateFilename}}
</a> </a>
<br> Modifier : <br> Modifier :
</template> </template>
@ -114,8 +105,7 @@
type="file" type="file"
:id="elementIdFor('piece_justificative_template')" :id="elementIdFor('piece_justificative_template')"
:name="nameFor('piece_justificative_template')" :name="nameFor('piece_justificative_template')"
:data-direct-upload-url="state.directUploadsUrl" @change="upload"
@change="update(changeLog)"
class="small-margin small"> class="small-margin small">
</div> </div>
<div class="cell" v-show="isCarte"> <div class="cell" v-show="isCarte">
@ -130,6 +120,7 @@
:id="elementIdFor('quartiers_prioritaires')" :id="elementIdFor('quartiers_prioritaires')"
:name="nameFor('quartiers_prioritaires')" :name="nameFor('quartiers_prioritaires')"
v-model="options.quartiers_prioritaires" v-model="options.quartiers_prioritaires"
@change="update"
class="small-margin small" class="small-margin small"
value="1"> value="1">
Quartiers prioritaires Quartiers prioritaires
@ -141,6 +132,7 @@
:id="elementIdFor('cadastres')" :id="elementIdFor('cadastres')"
:name="nameFor('cadastres')" :name="nameFor('cadastres')"
v-model="options.cadastres" v-model="options.cadastres"
@change="update"
class="small-margin small" class="small-margin small"
value="1"> value="1">
Cadastres Cadastres
@ -152,6 +144,7 @@
:id="elementIdFor('parcelles_agricoles')" :id="elementIdFor('parcelles_agricoles')"
:name="nameFor('parcelles_agricoles')" :name="nameFor('parcelles_agricoles')"
v-model="options.parcelles_agricoles" v-model="options.parcelles_agricoles"
@change="update"
class="small-margin small" class="small-margin small"
value="1"> value="1">
Parcelles Agricoles Parcelles Agricoles
@ -163,7 +156,6 @@
<DraggableItem <DraggableItem
v-for="(item, index) in typesDeChamp" v-for="(item, index) in typesDeChamp"
:state="stateForRepetition" :state="stateForRepetition"
:update="update"
:index="index" :index="index"
:item="item" :item="item"
:key="item.id" /> :key="item.id" />
@ -181,7 +173,7 @@
</div> </div>
<div class="meta"> <div class="meta">
<input type="hidden" :name="nameFor('order_place')" :value="index"> <input type="hidden" :name="nameFor('order_place')" :value="index">
<input type="hidden" :name="nameFor('id')" :value="item.id"> <input type="hidden" :name="nameFor('id')" :value="id">
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,13 +1,14 @@
export default { export default {
props: ['state', 'version', 'update', 'updateAll'], props: ['state', 'version'],
methods: { methods: {
addChamp() { addChamp() {
this.state.typesDeChamp.push({ this.state.typesDeChamp.push({
type_champ: 'text', type_champ: 'text',
drop_down_list: {}, types_de_champ: []
types_de_champ: [],
options: {}
}); });
},
save() {
this.state.flash.success();
} }
} }
}; };

View file

@ -10,14 +10,13 @@
</template> </template>
</button> </button>
<button class="button primary" @click.prevent="updateAll">Enregistrer</button> <button class="button primary" @click.prevent="save">Enregistrer</button>
</div> </div>
<Draggable :list="state.typesDeChamp" :options="{handle:'.handle'}"> <Draggable :list="state.typesDeChamp" :options="{handle:'.handle'}">
<DraggableItem <DraggableItem
v-for="(item, index) in state.typesDeChamp" v-for="(item, index) in state.typesDeChamp"
:state="state" :state="state"
:update="update"
:index="index" :index="index"
:item="item" :item="item"
:key="item.id" /> :key="item.id" />
@ -33,7 +32,7 @@
</template> </template>
</button> </button>
<button class="button primary" @click.prevent="updateAll">Enregistrer</button> <button class="button primary" @click.prevent="save">Enregistrer</button>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,6 +1,5 @@
import Vue from 'vue'; import Vue from 'vue';
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import { fire, debounce } from '@utils';
import DraggableItem from './DraggableItem'; import DraggableItem from './DraggableItem';
import DraggableList from './DraggableList'; import DraggableList from './DraggableList';
@ -16,109 +15,74 @@ addEventListener('DOMContentLoaded', () => {
}); });
function initEditor(el) { function initEditor(el) {
const { directUploadsUrl, dragIconUrl } = el.dataset; const { directUploadUrl, dragIconUrl, saveUrl } = el.dataset;
const state = { const state = {
typesDeChamp: JSON.parse(el.dataset.typesDeChamp), typesDeChamp: JSON.parse(el.dataset.typesDeChamp),
typesDeChampOptions: JSON.parse(el.dataset.typesDeChampOptions), typesDeChampOptions: JSON.parse(el.dataset.typesDeChampOptions),
directUploadsUrl, directUploadUrl,
dragIconUrl, dragIconUrl,
saveUrl,
isAnnotation: el.dataset.type === 'annotation', isAnnotation: el.dataset.type === 'annotation',
unsavedItems: new Set(), prefix: 'procedure',
unsavedInvalidItems: new Set(), inFlight: 0,
version: 1, flash: new Flash()
prefix: 'procedure'
}; };
// We add an initial type de champ here if form is empty
if (state.typesDeChamp.length === 0) {
state.typesDeChamp.push({
type_champ: 'text',
types_de_champ: []
});
}
new Vue({ new Vue({
el, el,
data: { data: {
state, state
update: null
}, },
render(h) { render(h) {
return h(DraggableList, { return h(DraggableList, {
props: { props: {
state: this.state, state: this.state
update: this.update,
updateAll: this.updateAll
} }
}); });
},
mounted() {
const [update, updateAll] = createUpdateFunctions(
this,
state.isAnnotation
);
this.update = update;
this.updateAll = updateAll;
// We add an initial type de champ here if form is empty
if (this.state.typesDeChamp.length === 0) {
this.state.typesDeChamp.push({
type_champ: 'text',
types_de_champ: []
});
}
} }
}); });
} }
function createUpdateFunctions(app, isAnnotation) { class Flash {
let isSaving = false; constructor(isAnnotation) {
const form = app.$el.closest('form'); this.element = document.querySelector('#flash_messages');
this.isAnnotation = isAnnotation;
const update = ([id, isValid], refresh = true) => { }
app.state.unsavedItems.add(id); success() {
if (isValid) { if (this.isAnnotation) {
app.state.unsavedInvalidItems.delete(id); this.add('Annotations privées enregistrées.');
} else { } else {
app.state.unsavedInvalidItems.add(id); this.add('Formulaire enregistré.');
} }
if (refresh) { }
app.state.version += 1; error(message) {
} this.add(message, true);
updateAll(); }
}; 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>`;
const updateAll = debounce(() => { this.element.innerHTML = html;
if (isSaving) {
updateAll();
} else if (
app.state.typesDeChamp.length > 0 &&
app.state.unsavedInvalidItems.size === 0
) {
isSaving = true;
app.state.unsavedItems.clear();
app.state.version += 1;
fire(form, 'submit');
}
}, 500);
addEventListener('ProcedureUpdated', event => { setTimeout(() => {
const { types_de_champ, types_de_champ_private } = event.detail; this.clear();
}, 6000);
app.state.typesDeChamp = isAnnotation
? types_de_champ_private
: types_de_champ;
isSaving = false;
updateFileInputs();
});
return [update, updateAll];
}
// This is needed du to the way ActiveStorage javascript integration works.
// It is built to be used with traditional forms. Another way would be to not use
// high level ActiveStorage abstractions (and maybe this is what we should do in the future).
function updateFileInputs() {
for (let element of document.querySelectorAll('.direct-upload')) {
let hiddenInput = element.nextElementSibling;
let fileInput = hiddenInput.nextElementSibling;
element.remove();
hiddenInput.remove();
fileInput.value = '';
fileInput.removeAttribute('disabled');
} }
} }

View file

@ -179,6 +179,14 @@ class TypeDeChamp < ApplicationRecord
end end
end end
def drop_down_list_value
drop_down_list&.value
end
def drop_down_list_value=(value)
self.drop_down_list_attributes = { value: value }
end
private private
def setup_procedure def setup_procedure

View file

@ -1,3 +0,0 @@
<%= render_flash timeout: 6000, fixed: true %>
<%= fire_event(:ProcedureUpdated, procedure_data(@procedure.reload)) %>

View file

@ -367,6 +367,8 @@ Rails.application.routes.draw do
get 'champs' get 'champs'
get 'annotations' get 'annotations'
end end
resources :types_de_champ, only: [:create, :update, :destroy]
end end
resources :services, except: [:show] do resources :services, except: [:show] do

View file

@ -102,7 +102,8 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ' fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ'
expect(page).to have_content('Formulaire mis à jour') blur
expect(page).to have_content('Formulaire enregistré')
within '.footer' do within '.footer' do
click_on 'Ajouter un champ' click_on 'Ajouter un champ'
@ -129,7 +130,8 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
page.refresh page.refresh
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ' fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ'
expect(page).to have_content('Formulaire mis à jour') blur
expect(page).to have_content('Formulaire enregistré')
click_on Procedure.last.libelle click_on Procedure.last.libelle
click_on 'onglet-pieces' click_on 'onglet-pieces'

View file

@ -1,6 +1,6 @@
require 'spec_helper' require 'spec_helper'
feature 'As an administrateur I edit procedure', js: true do feature 'As an administrateur I can edit types de champ', js: true do
let(:administrateur) { procedure.administrateur } let(:administrateur) { procedure.administrateur }
let(:procedure) { create(:procedure) } let(:procedure) { create(:procedure) }
@ -18,14 +18,15 @@ feature 'As an administrateur I edit procedure', js: true do
end end
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ' fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
expect(page).to have_content('Formulaire mis à jour') blur
expect(page).to have_content('Formulaire enregistré')
page.refresh page.refresh
within '.footer' do within '.footer' do
click_on 'Enregistrer' click_on 'Enregistrer'
end end
expect(page).to have_content('Formulaire mis à jour') expect(page).to have_content('Formulaire enregistré')
end end
it "Add multiple champs" do it "Add multiple champs" do
@ -34,33 +35,29 @@ feature 'As an administrateur I edit procedure', js: true do
click_on 'Ajouter un champ' click_on 'Ajouter un champ'
click_on 'Ajouter un champ' click_on 'Ajouter un champ'
end end
expect(page).not_to have_content('Le libellé doit être rempli.') expect(page).not_to have_content('Formulaire enregistré')
expect(page).not_to have_content('Modifications non sauvegardées.')
expect(page).not_to have_content('Formulaire mis à jour')
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ 0' fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ 0'
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 1'
blur
expect(page).to have_content('Formulaire enregistré')
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
expect(page).to have_selector('#procedure_types_de_champ_attributes_1_libelle') expect(page).to have_selector('#procedure_types_de_champ_attributes_1_libelle')
expect(page).to have_selector('#procedure_types_de_champ_attributes_2_libelle') expect(page).to have_selector('#procedure_types_de_champ_attributes_2_libelle')
expect(page).to have_selector('#procedure_types_de_champ_attributes_3_libelle') expect(page).to have_selector('#procedure_types_de_champ_attributes_3_libelle')
expect(page).to have_content('Le libellé doit être rempli.') within '.draggable-item-2' do
expect(page).to have_content('Modifications non sauvegardées.')
expect(page).not_to have_content('Formulaire mis à jour')
fill_in 'procedure_types_de_champ_attributes_2_libelle', with: 'libellé de champ 2'
within '.draggable-item-3' do
click_on 'Supprimer' click_on 'Supprimer'
end end
expect(page).to have_content('Le libellé doit être rempli.') expect(page).not_to have_selector('#procedure_types_de_champ_attributes_3_libelle')
expect(page).to have_content('Modifications non sauvegardées.') fill_in 'procedure_types_de_champ_attributes_2_libelle', with: 'libellé de champ 2'
expect(page).not_to have_content('Formulaire mis à jour') blur
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 1' expect(page).to have_content('Formulaire enregistré')
expect(page).to have_content('Supprimer', count: 3)
expect(page).not_to have_content('Le libellé doit être rempli.')
expect(page).not_to have_content('Modifications non sauvegardées.')
expect(page).to have_content('Formulaire mis à jour')
page.refresh page.refresh
expect(page).to have_content('Supprimer', count: 3) expect(page).to have_content('Supprimer', count: 3)
@ -68,11 +65,12 @@ feature 'As an administrateur I edit procedure', js: true do
it "Remove champs" do it "Remove champs" do
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ' fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
expect(page).to have_content('Formulaire mis à jour') blur
expect(page).to have_content('Formulaire enregistré')
page.refresh page.refresh
click_on 'Supprimer' click_on 'Supprimer'
expect(page).to have_content('Formulaire mis à jour') expect(page).to have_content('Formulaire enregistré')
expect(page).not_to have_content('Supprimer') expect(page).not_to have_content('Supprimer')
page.refresh page.refresh
@ -82,19 +80,21 @@ feature 'As an administrateur I edit procedure', js: true do
it "Only add valid champs" do it "Only add valid champs" do
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_description') expect(page).to have_selector('#procedure_types_de_champ_attributes_0_description')
fill_in 'procedure_types_de_champ_attributes_0_description', with: 'déscription du champ' fill_in 'procedure_types_de_champ_attributes_0_description', with: 'déscription du champ'
expect(page).to have_content('Le libellé doit être rempli.') blur
expect(page).not_to have_content('Formulaire mis à jour') expect(page).not_to have_content('Formulaire enregistré')
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ' fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
expect(page).to have_content('Formulaire mis à jour') blur
expect(page).to have_content('Formulaire enregistré')
end end
it "Add repetition champ" do it "Add repetition champ" do
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
select('Bloc répétable', from: 'procedure_types_de_champ_attributes_0_type_champ') select('Bloc répétable', from: 'procedure_types_de_champ_attributes_0_type_champ')
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ' fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
blur
expect(page).to have_content('Formulaire mis à jour') expect(page).to have_content('Formulaire enregistré')
page.refresh page.refresh
within '.flex-grow' do within '.flex-grow' do
@ -102,8 +102,9 @@ feature 'As an administrateur I edit procedure', js: true do
end end
fill_in 'procedure_types_de_champ_attributes_0_types_de_champ_attributes_0_libelle', with: 'libellé de champ 1' fill_in 'procedure_types_de_champ_attributes_0_types_de_champ_attributes_0_libelle', with: 'libellé de champ 1'
blur
expect(page).to have_content('Formulaire mis à jour') expect(page).to have_content('Formulaire enregistré')
expect(page).to have_content('Supprimer', count: 2) expect(page).to have_content('Supprimer', count: 2)
within '.footer' do within '.footer' do
@ -112,6 +113,7 @@ feature 'As an administrateur I edit procedure', js: true do
select('Bloc répétable', from: 'procedure_types_de_champ_attributes_1_type_champ') select('Bloc répétable', from: 'procedure_types_de_champ_attributes_1_type_champ')
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 2' fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 2'
blur
expect(page).to have_content('Supprimer', count: 3) expect(page).to have_content('Supprimer', count: 3)
end end

View file

@ -58,6 +58,10 @@ module FeatureHelpers
# Procedure contact infos in the footer # Procedure contact infos in the footer
expect(page).to have_content(procedure.service.email) expect(page).to have_content(procedure.service.email)
end end
def blur
page.find('body').click
end
end end
RSpec.configure do |config| RSpec.configure do |config|