[Types de Champ Editeur] Save on change and only edited model
This commit is contained in:
parent
dea78e2e4e
commit
5da5f75c5f
14 changed files with 351 additions and 254 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<%= render_flash timeout: 6000, fixed: true %>
|
||||
|
||||
<%= fire_event(:ProcedureUpdated, procedure_data(@procedure.reload)) %>
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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|
|
||||
|
|
Loading…
Reference in a new issue