From 0d35295d4e02df445e94b3862e1a21dfe7898688 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 14 Nov 2018 16:26:00 +0100 Subject: [PATCH] New champs editor --- app/assets/images/icons/drag.svg | 1 + app/assets/stylesheets/new_design/forms.scss | 6 + .../new_design/procedure_champs_editor.scss | 137 +++++++++++++++ .../administrateur/DraggableItem.js | 131 ++++++++++++++ .../administrateur/DraggableItem.vue | 165 ++++++++++++++++++ .../administrateur/DraggableList.js | 12 ++ .../administrateur/DraggableList.vue | 42 +++++ .../administrateur/champs-editor.js | 109 ++++++++++++ app/javascript/packs/application.js | 2 + 9 files changed, 605 insertions(+) create mode 100644 app/assets/images/icons/drag.svg create mode 100644 app/assets/stylesheets/new_design/procedure_champs_editor.scss create mode 100644 app/javascript/new_design/administrateur/DraggableItem.js create mode 100644 app/javascript/new_design/administrateur/DraggableItem.vue create mode 100644 app/javascript/new_design/administrateur/DraggableList.js create mode 100644 app/javascript/new_design/administrateur/DraggableList.vue create mode 100644 app/javascript/new_design/administrateur/champs-editor.js diff --git a/app/assets/images/icons/drag.svg b/app/assets/images/icons/drag.svg new file mode 100644 index 000000000..a29233885 --- /dev/null +++ b/app/assets/images/icons/drag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/new_design/forms.scss b/app/assets/stylesheets/new_design/forms.scss index d8b4af32f..95beff55e 100644 --- a/app/assets/stylesheets/new_design/forms.scss +++ b/app/assets/stylesheets/new_design/forms.scss @@ -107,6 +107,12 @@ } } + input[type=checkbox] { + &.small-margin { + margin-bottom: $default-padding / 2; + } + } + input[type=text]:not([data-address='true']), input[type=email], input[type=password], diff --git a/app/assets/stylesheets/new_design/procedure_champs_editor.scss b/app/assets/stylesheets/new_design/procedure_champs_editor.scss new file mode 100644 index 000000000..5e1241f38 --- /dev/null +++ b/app/assets/stylesheets/new_design/procedure_champs_editor.scss @@ -0,0 +1,137 @@ +@import "colors"; + +#champs-editor { + .spinner { + margin-right: auto; + margin-left: auto; + margin-top: 80px; + } +} + +.draggable-item { + display: flex; + flex-direction: column; + justify-content: flex-start; + + border: 1px solid $border-grey; + border-radius: 5px; + margin-bottom: 10px; + width: 100%; + + .handle { + cursor: ns-resize; + margin-right: 10px; + margin-top: 8px; + } + + .error-message { + text-align: center; + flex-grow: 1; + font-size: 14px; + color: $light-grey; + display: flex; + align-items: center; + flex-direction: column; + justify-content: space-around; + + .content { + background-color: $medium-red; + border-radius: 8px; + padding: 4px 10px; + } + } + + &.type-header-section { + background-color: $blue; + + label { + color: $light-grey; + } + } + + &:not(.type-header-section) { + input.error { + border: 1px solid $medium-red; + } + } + + .column { + display: flex; + justify-content: flex-start; + flex-direction: column; + + &.shift-left { + margin-left: 35px; + } + } + + .row { + display: flex; + justify-content: flex-start; + + &.section { + padding: 10px 10px 0 10px; + margin-bottom: 8px; + } + + &.hr { + border-bottom: 1px solid $border-grey; + + &.head { + padding-bottom: 10px; + } + } + + &.shift-left { + margin-left: 35px; + } + + &.head { + select { + margin-bottom: 0px; + } + } + + &.delete { + flex-grow: 1; + display: flex; + justify-content: flex-end; + } + } + + .cell { + margin-right: 20px; + + &.small { + width: 90px; + } + + &.libelle { + width: 300px; + } + + label { + margin-bottom: 8px; + text-transform: uppercase; + font-size: 12px; + } + } + + .carte-options { + label { + font-weight: initial; + } + } + + .inline { + display: inline; + } +} + +.header, +.footer { + display: flex; + justify-content: space-between; + margin-top: 30px; + margin-bottom: 30px; +} diff --git a/app/javascript/new_design/administrateur/DraggableItem.js b/app/javascript/new_design/administrateur/DraggableItem.js new file mode 100644 index 000000000..f9a55edbb --- /dev/null +++ b/app/javascript/new_design/administrateur/DraggableItem.js @@ -0,0 +1,131 @@ +export default { + props: ['state', 'update', 'index', 'item', 'prefix'], + computed: { + isDirty() { + return ( + this.state.version && + this.state.unsavedInvalidItems.size > 0 && + this.state.unsavedItems.has(this.itemId) + ); + }, + isInvalid() { + if (this.deleted) { + return false; + } + if (this.libelle) { + return !this.libelle.trim(); + } + return true; + }, + itemId() { + return this.item.id || this.clientId; + }, + changeLog() { + return [this.itemId, !this.isInvalid]; + }, + 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() { + return [ + 'drop_down_list', + 'multiple_drop_down_list', + 'linked_drop_down_list' + ].includes(this.typeChamp); + }, + isFile() { + return this.typeChamp === 'piece_justificative'; + }, + isCarte() { + return this.typeChamp === 'carte'; + }, + isExplication() { + return this.typeChamp === 'explication'; + }, + isHeaderSection() { + return this.typeChamp === 'header_section'; + }, + options() { + const options = this.item.options; + for (let key of Object.keys(options)) { + options[key] = castBoolean(options[key]); + } + return options; + }, + attribute() { + if (this.state.isAnnotation) { + return 'types_de_champ_private_attributes'; + } else { + return 'types_de_champ_attributes'; + } + } + }, + data() { + return { + typeChamp: this.item.type_champ, + libelle: this.item.libelle, + mandatory: this.item.mandatory, + description: this.item.description, + dropDownList: this.item.drop_down_list.value, + deleted: false, + clientId: `id-${clientIds++}` + }; + }, + created() { + for (let path of PATHS_TO_WATCH) { + this.$watch(path, () => this.update(this.changeLog)); + } + }, + mounted() { + if (this.isInvalid) { + this.update(this.changeLog, false); + } + }, + methods: { + removeChamp(item) { + if (item.id) { + this.deleted = true; + } else { + const index = this.state.typesDeChamp.indexOf(item); + this.state.typesDeChamp.splice(index, 1); + this.update([this.itemId, true]); + } + }, + nameFor(name) { + return `${this.prefix}[${this.attribute}][${this.index}][${name}]`; + }, + elementIdFor(name) { + return `${this.prefix}_${this.attribute}_${this.index}_${name}`; + } + } +}; + +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; diff --git a/app/javascript/new_design/administrateur/DraggableItem.vue b/app/javascript/new_design/administrateur/DraggableItem.vue new file mode 100644 index 000000000..93849ed8d --- /dev/null +++ b/app/javascript/new_design/administrateur/DraggableItem.vue @@ -0,0 +1,165 @@ + + + diff --git a/app/javascript/new_design/administrateur/DraggableList.js b/app/javascript/new_design/administrateur/DraggableList.js new file mode 100644 index 000000000..b680daa52 --- /dev/null +++ b/app/javascript/new_design/administrateur/DraggableList.js @@ -0,0 +1,12 @@ +export default { + props: ['state', 'version', 'update', 'updateAll'], + methods: { + addChamp() { + this.state.typesDeChamp.push({ + type_champ: 'text', + drop_down_list: {}, + options: {} + }); + } + } +}; diff --git a/app/javascript/new_design/administrateur/DraggableList.vue b/app/javascript/new_design/administrateur/DraggableList.vue new file mode 100644 index 000000000..7832e565b --- /dev/null +++ b/app/javascript/new_design/administrateur/DraggableList.vue @@ -0,0 +1,42 @@ + + + diff --git a/app/javascript/new_design/administrateur/champs-editor.js b/app/javascript/new_design/administrateur/champs-editor.js new file mode 100644 index 000000000..6bfb9d43d --- /dev/null +++ b/app/javascript/new_design/administrateur/champs-editor.js @@ -0,0 +1,109 @@ +import Vue from 'vue'; +import Draggable from 'vuedraggable'; +import { fire, debounce } from '@utils'; + +import DraggableItem from './DraggableItem'; +import DraggableList from './DraggableList'; + +Vue.component('Draggable', Draggable); +Vue.component('DraggableItem', DraggableItem); + +addEventListener('DOMContentLoaded', () => { + const el = document.querySelector('#champs-editor'); + const { directUploadsUrl, dragIconUrl } = el.dataset; + + const state = { + typesDeChamp: JSON.parse(el.dataset.typesDeChamp), + typesDeChampOptions: JSON.parse(el.dataset.typesDeChampOptions), + directUploadsUrl, + dragIconUrl, + isAnnotation: el.dataset.type === 'annotation', + unsavedItems: new Set(), + unsavedInvalidItems: new Set(), + version: 1 + }; + + new Vue({ + el, + data: { + state, + update: null + }, + render(h) { + return h(DraggableList, { + props: { + state: this.state, + update: this.update, + updateAll: this.updateAll + } + }); + }, + mounted() { + const [update, updateAll] = createUpdateFunctions( + this, + state.isAnnotation + ); + + this.update = update; + this.updateAll = updateAll; + } + }); +}); + +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); + } else { + app.state.unsavedInvalidItems.add(id); + } + if (refresh) { + app.state.version += 1; + } + updateAll(); + }; + + 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); + + 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'); + } +} diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 403078f86..033d88faf 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -22,6 +22,8 @@ import '../new_design/select2'; import '../new_design/champs/carte'; import '../new_design/champs/linked-drop-down-list'; +import '../new_design/administrateur/champs-editor'; + import { toggleCondidentielExplanation } from '../new_design/avis'; import { scrollMessagerie } from '../new_design/messagerie'; import { showMotivation, motivationCancel } from '../new_design/state-button';