From 5da5f75c5f74f783db7a5e77933c72320a23bbaf Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 6 Feb 2019 18:19:27 +0100 Subject: [PATCH] [Types de Champ Editeur] Save on change and only edited model --- .../procedures_controller.rb | 47 +--- .../types_de_champ_controller.rb | 76 +++++++ app/helpers/procedure_helper.rb | 19 +- .../administrateur/DraggableItem.js | 206 +++++++++++++----- .../administrateur/DraggableItem.vue | 42 ++-- .../administrateur/DraggableList.js | 9 +- .../administrateur/DraggableList.vue | 5 +- .../administrateur/champs-editor.js | 124 ++++------- app/models/type_de_champ.rb | 8 + .../procedures/update.js.erb | 3 - config/routes.rb | 2 + .../features/admin/procedure_creation_spec.rb | 6 +- ...cedures_spec.rb => types_de_champ_spec.rb} | 54 ++--- spec/support/feature_helpers.rb | 4 + 14 files changed, 351 insertions(+), 254 deletions(-) create mode 100644 app/controllers/new_administrateur/types_de_champ_controller.rb delete mode 100644 app/views/new_administrateur/procedures/update.js.erb rename spec/features/new_administrateur/{procedures_spec.rb => types_de_champ_spec.rb} (67%) diff --git a/app/controllers/new_administrateur/procedures_controller.rb b/app/controllers/new_administrateur/procedures_controller.rb index ff4734397..e51e75aa0 100644 --- a/app/controllers/new_administrateur/procedures_controller.rb +++ b/app/controllers/new_administrateur/procedures_controller.rb @@ -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 diff --git a/app/controllers/new_administrateur/types_de_champ_controller.rb b/app/controllers/new_administrateur/types_de_champ_controller.rb new file mode 100644 index 000000000..c4bf50110 --- /dev/null +++ b/app/controllers/new_administrateur/types_de_champ_controller.rb @@ -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 diff --git a/app/helpers/procedure_helper.rb b/app/helpers/procedure_helper.rb index fe3395201..3222f0064 100644 --- a/app/helpers/procedure_helper.rb +++ b/app/helpers/procedure_helper.rb @@ -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, diff --git a/app/javascript/new_design/administrateur/DraggableItem.js b/app/javascript/new_design/administrateur/DraggableItem.js index 95d284120..ea6c1361b 100644 --- a/app/javascript/new_design/administrateur/DraggableItem.js +++ b/app/javascript/new_design/administrateur/DraggableItem.js @@ -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); + } + }); + }); +} diff --git a/app/javascript/new_design/administrateur/DraggableItem.vue b/app/javascript/new_design/administrateur/DraggableItem.vue index fbb70a5c8..1351dc395 100644 --- a/app/javascript/new_design/administrateur/DraggableItem.vue +++ b/app/javascript/new_design/administrateur/DraggableItem.vue @@ -1,6 +1,6 @@