[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
class ProceduresController < AdministrateurController
before_action :retrieve_procedure, only: [:champs, :annotations, :update]
before_action :procedure_locked?, only: [:champs, :annotations, :update]
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
}
before_action :retrieve_procedure, only: [:champs, :annotations]
before_action :procedure_locked?, only: [:champs, :annotations]
def apercu
@dossier = procedure_without_control.new_dossier
@tab = apercu_tab
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
def apercu_tab
@ -53,12 +17,5 @@ module NewAdministrateur
def procedure_without_control
Procedure.find(params[:id])
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

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",
types_de_champ_options: types_de_champ_options.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")
}
end
@ -49,18 +50,12 @@ module ProcedureHelper
type: "annotation",
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,
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")
}
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
TOGGLES = {
@ -79,14 +74,12 @@ module ProcedureHelper
types_de_champ
end
TYPES_DE_CHAMP_INCLUDE = { drop_down_list: { only: :value } }
TYPES_DE_CHAMP_BASE = {
except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private],
methods: [:piece_justificative_template_filename, :piece_justificative_template_url],
include: TYPES_DE_CHAMP_INCLUDE
methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value]
}
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)
types_de_champ.includes(:drop_down_list,

View file

@ -1,40 +1,23 @@
import { getJSON, debounce } from '@utils';
import { DirectUpload } from 'activestorage';
export default {
props: ['state', 'update', 'index', 'item'],
props: ['state', 'index', 'item'],
computed: {
isDirty() {
return (
this.state.version &&
this.state.unsavedInvalidItems.size > 0 &&
this.state.unsavedItems.has(this.itemId)
);
},
isInvalid() {
isValid() {
if (this.deleted) {
return false;
return true;
}
if (this.libelle) {
return !this.libelle.trim();
return !!this.libelle.trim();
}
return true;
},
itemId() {
return this.item.id || this.clientId;
},
changeLog() {
return [this.itemId, !this.isInvalid];
return false;
},
itemClassName() {
const classNames = [`draggable-item-${this.index}`];
if (this.isHeaderSection) {
classNames.push('type-header-section');
}
if (this.isDirty) {
if (this.isInvalid) {
classNames.push('invalid');
} else {
classNames.push('dirty');
}
}
return classNames.join(' ');
},
isDropDown() {
@ -73,6 +56,47 @@ export default {
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() {
return this.item.types_de_champ;
},
@ -85,39 +109,46 @@ export default {
return Object.assign({}, this.state, {
typesDeChamp: this.typesDeChamp,
typesDeChampOptions: this.typesDeChampOptions,
prefix: `${this.state.prefix}[${this.attribute}][${this.index}]`
prefix: `${this.state.prefix}[${this.attribute}][${this.index}]`,
parentId: this.id
});
}
},
data() {
return {
id: this.item.id,
typeChamp: this.item.type_champ,
libelle: this.item.libelle,
mandatory: this.item.mandatory,
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,
clientId: `id-${clientIds++}`
isSaving: false,
isUploading: false,
hasChanges: false
};
},
created() {
for (let path of PATHS_TO_WATCH) {
this.$watch(path, () => this.update(this.changeLog));
watch: {
index() {
this.update();
}
},
mounted() {
if (this.isInvalid) {
this.update(this.changeLog, false);
}
created() {
this.debouncedSave = debounce(() => this.save(), 500);
this.debouncedUpload = debounce(evt => this.upload(evt), 500);
},
methods: {
removeChamp(item) {
if (item.id) {
removeChamp() {
if (this.id) {
this.deleted = true;
this.debouncedSave();
} else {
const index = this.state.typesDeChamp.indexOf(item);
const index = this.state.typesDeChamp.indexOf(this.item);
this.state.typesDeChamp.splice(index, 1);
this.update([this.itemId, true]);
}
},
nameFor(name) {
@ -132,6 +163,76 @@ export default {
type_champ: 'text',
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'
];
const PATHS_TO_WATCH = [
'typeChamp',
'libelle',
'mandatory',
'description',
'dropDownList',
'options.quartiers_prioritaires',
'options.cadastres',
'options.parcelles_agricoles',
'index',
'deleted'
];
function castBoolean(value) {
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>
<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">
</div>
@ -14,6 +14,7 @@
:id="elementIdFor('type_champ')"
:name="nameFor('type_champ')"
v-model="typeChamp"
@change="update"
class="small-margin small inline">
<option v-for="option in state.typesDeChampOptions" :key="option[1]" :value="option[1]">
{{ option[0] }}
@ -21,21 +22,7 @@
</select>
</div>
<div class="flex justify-start delete">
<div v-if="isDirty" class="error-message">
<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)">
<button class="button danger" @click.prevent="removeChamp">
Supprimer
</button>
</div>
@ -51,8 +38,9 @@
:id="elementIdFor('libelle')"
:name="nameFor('libelle')"
v-model="libelle"
@change="update"
class="small-margin small"
:class="{ error: isDirty && isInvalid }">
:class="{ error: hasChanges && !isValid }">
</div>
<div class="cell" v-show="!isHeaderSection && !isExplication && !state.isAnnotation">
@ -65,6 +53,7 @@
:id="elementIdFor('mandatory')"
:name="nameFor('mandatory')"
v-model="mandatory"
@change="update"
class="small-margin small"
value="1">
</div>
@ -78,6 +67,7 @@
:id="elementIdFor('description')"
:name="nameFor('description')"
v-model="description"
@change="update"
rows=3
cols=40
class="small-margin small">
@ -93,7 +83,8 @@
<textarea
:id="elementIdFor('drop_down_list')"
:name="nameFor('drop_down_list_attributes[value]')"
v-model="dropDownList"
v-model="dropDownListValue"
@change="update"
rows=3
cols=40
placeholder="Ecrire une valeur par ligne et --valeur-- pour un séparateur."
@ -104,9 +95,9 @@
<label :for="elementIdFor('piece_justificative_template')">
Modèle
</label>
<template v-if="item.piece_justificative_template_url">
<a :href="item.piece_justificative_template_url" target="_blank">
{{item.piece_justificative_template_filename}}
<template v-if="pieceJustificativeTemplateUrl">
<a :href="pieceJustificativeTemplateUrl" target="_blank">
{{pieceJustificativeTemplateFilename}}
</a>
<br> Modifier :
</template>
@ -114,8 +105,7 @@
type="file"
:id="elementIdFor('piece_justificative_template')"
:name="nameFor('piece_justificative_template')"
:data-direct-upload-url="state.directUploadsUrl"
@change="update(changeLog)"
@change="upload"
class="small-margin small">
</div>
<div class="cell" v-show="isCarte">
@ -130,6 +120,7 @@
:id="elementIdFor('quartiers_prioritaires')"
:name="nameFor('quartiers_prioritaires')"
v-model="options.quartiers_prioritaires"
@change="update"
class="small-margin small"
value="1">
Quartiers prioritaires
@ -141,6 +132,7 @@
:id="elementIdFor('cadastres')"
:name="nameFor('cadastres')"
v-model="options.cadastres"
@change="update"
class="small-margin small"
value="1">
Cadastres
@ -152,6 +144,7 @@
:id="elementIdFor('parcelles_agricoles')"
:name="nameFor('parcelles_agricoles')"
v-model="options.parcelles_agricoles"
@change="update"
class="small-margin small"
value="1">
Parcelles Agricoles
@ -163,7 +156,6 @@
<DraggableItem
v-for="(item, index) in typesDeChamp"
:state="stateForRepetition"
:update="update"
:index="index"
:item="item"
:key="item.id" />
@ -181,7 +173,7 @@
</div>
<div class="meta">
<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>
</template>

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import Vue from 'vue';
import Draggable from 'vuedraggable';
import { fire, debounce } from '@utils';
import DraggableItem from './DraggableItem';
import DraggableList from './DraggableList';
@ -16,109 +15,74 @@ addEventListener('DOMContentLoaded', () => {
});
function initEditor(el) {
const { directUploadsUrl, dragIconUrl } = el.dataset;
const { directUploadUrl, dragIconUrl, saveUrl } = el.dataset;
const state = {
typesDeChamp: JSON.parse(el.dataset.typesDeChamp),
typesDeChampOptions: JSON.parse(el.dataset.typesDeChampOptions),
directUploadsUrl,
directUploadUrl,
dragIconUrl,
saveUrl,
isAnnotation: el.dataset.type === 'annotation',
unsavedItems: new Set(),
unsavedInvalidItems: new Set(),
version: 1,
prefix: 'procedure'
prefix: 'procedure',
inFlight: 0,
flash: new Flash()
};
// 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({
el,
data: {
state,
update: null
state
},
render(h) {
return h(DraggableList, {
props: {
state: this.state,
update: this.update,
updateAll: this.updateAll
state: this.state
}
});
},
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) {
let isSaving = false;
const form = app.$el.closest('form');
const update = ([id, isValid], refresh = true) => {
app.state.unsavedItems.add(id);
if (isValid) {
app.state.unsavedInvalidItems.delete(id);
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 {
app.state.unsavedInvalidItems.add(id);
this.add('Formulaire enregistré.');
}
if (refresh) {
app.state.version += 1;
}
updateAll();
};
}
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>`;
const updateAll = debounce(() => {
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);
this.element.innerHTML = html;
addEventListener('ProcedureUpdated', event => {
const { types_de_champ, types_de_champ_private } = event.detail;
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');
setTimeout(() => {
this.clear();
}, 6000);
}
}

View file

@ -179,6 +179,14 @@ class TypeDeChamp < ApplicationRecord
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
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 'annotations'
end
resources :types_de_champ, only: [:create, :update, :destroy]
end
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')
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
click_on 'Ajouter un champ'
@ -129,7 +130,8 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
page.refresh
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 'onglet-pieces'

View file

@ -1,6 +1,6 @@
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(:procedure) { create(:procedure) }
@ -18,14 +18,15 @@ feature 'As an administrateur I edit procedure', js: true do
end
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'
expect(page).to have_content('Formulaire mis à jour')
blur
expect(page).to have_content('Formulaire enregistré')
page.refresh
within '.footer' do
click_on 'Enregistrer'
end
expect(page).to have_content('Formulaire mis à jour')
expect(page).to have_content('Formulaire enregistré')
end
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'
end
expect(page).not_to have_content('Le libellé doit être rempli.')
expect(page).not_to have_content('Modifications non sauvegardées.')
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 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_1_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_content('Le libellé doit être rempli.')
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
within '.draggable-item-2' do
click_on 'Supprimer'
end
expect(page).to have_content('Le libellé doit être rempli.')
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_1_libelle', with: 'libellé de champ 1'
expect(page).not_to have_selector('#procedure_types_de_champ_attributes_3_libelle')
fill_in 'procedure_types_de_champ_attributes_2_libelle', with: 'libellé de champ 2'
blur
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
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
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
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')
page.refresh
@ -82,19 +80,21 @@ feature 'As an administrateur I edit procedure', js: true do
it "Only add valid champs" do
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'
expect(page).to have_content('Le libellé doit être rempli.')
expect(page).not_to have_content('Formulaire mis à jour')
blur
expect(page).not_to have_content('Formulaire enregistré')
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
it "Add repetition champ" do
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')
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
within '.flex-grow' do
@ -102,8 +102,9 @@ feature 'As an administrateur I edit procedure', js: true do
end
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)
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')
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 2'
blur
expect(page).to have_content('Supprimer', count: 3)
end

View file

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