diff --git a/src/components/feature/FeatureAttachmentForm.vue b/src/components/feature/FeatureAttachmentForm.vue index 83343c7935dab9d69dee36ccbde5b2315927da43..eb7f1aa25b5de25e34e80ea718e26d63373efe01 100644 --- a/src/components/feature/FeatureAttachmentForm.vue +++ b/src/components/feature/FeatureAttachmentForm.vue @@ -23,7 +23,6 @@ :name="form.title.html_name" :id="form.title.id_for_label" v-model="form.title.value" - /> <ul :id="form.title.id_for_error" class="errorlist"> <li v-for="error in form.title.errors" :key="error"> @@ -46,6 +45,7 @@ <input @change="onFileChange" type="file" + accept="application/pdf, image/jpeg, image/png" style="display: none" :name="form.attachment_file.html_name" :id="'attachment_file' + attachmentForm.dataKey" @@ -118,21 +118,23 @@ export default { attachmentForm(newValue) { this.initForm(newValue); }, + //* utilisation de watcher, car @change aurait un délai "form.title.value": function (newValue, oldValue) { - if (oldValue != ''){ - if (newValue != oldValue){ + if (oldValue != "") { + if (newValue != oldValue) { this.updateStore(); } } }, "form.info.value": function (newValue, oldValue) { - if (oldValue != ''){ - if (newValue != oldValue){ + if (oldValue != "") { + if (newValue != oldValue) { this.updateStore(); } } }, }, + methods: { initForm(attachmentForm) { for (let el in attachmentForm) { @@ -152,10 +154,7 @@ export default { this.attachmentForm.dataKey ); if (this.form.id.value) - this.$store.commit( - "feature/DELETE_ATTACHMENTS", - this.form.id.value - ); + this.$store.commit("feature/DELETE_ATTACHMENTS", this.form.id.value); }, updateStore() { @@ -166,22 +165,54 @@ export default { attachment_file: this.form.attachment_file.value, info: this.form.info.value, fileToImport: this.fileToImport, - } + }; this.$store.commit("feature/UPDATE_ATTACHMENT_FORM", data); - if (data.id){ - this.$store.commit( - "feature/PUT_ATTACHMENTS", - data - ); + if (data.id) { + this.$store.commit("feature/PUT_ATTACHMENTS", data); } }, + validateImgFile(files, handleFile) { + let url = window.URL || window.webkitURL; + let image = new Image(); + image.onload = function () { + handleFile(true); + }; + image.onerror = function () { + handleFile(false); + }; + image.src = url.createObjectURL(files); + URL.revokeObjectURL(image.src); + }, + onFileChange(e) { + // * read image file const files = e.target.files || e.dataTransfer.files; - if (!files.length) return; - this.fileToImport = files[0]; //* store file to import - this.form.attachment_file.value = files[0].name; //* add name to the form for display, in order to match format return from API - this.updateStore(); + + const _this = this; //* 'this' is different in onload function + function handleFile(isValid) { + if (isValid) { + _this.fileToImport = files[0]; //* store the file to post later + _this.form.attachment_file.value = files[0].name; //* add name to the form for display, in order to match format return from API + _this.updateStore(); + _this.form.attachment_file.errors = []; + } else { + _this.form.attachment_file.errors.push( + "Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu." + ); + } + } + + if (files.length) { + //* exception for pdf + if (files[0].type === "application/pdf") { + handleFile(true); + } else { + this.form.attachment_file.errors = []; + //* check if file is an image and pass callback to handle file + this.validateImgFile(files[0], handleFile); + } + } }, checkForm() { diff --git a/src/components/feature/FeatureListTable.vue b/src/components/feature/FeatureListTable.vue index ed7f0dac060041a1badf0b1f4137cf458d8ba7b6..c7ebb5c23e78db573e2df94005f12a000d1f2a3b 100644 --- a/src/components/feature/FeatureListTable.vue +++ b/src/components/feature/FeatureListTable.vue @@ -71,8 +71,7 @@ type="checkbox" :id="feature.id" :value="feature.id" - v-model="checkedFeatures" - :checked="checkedFeatures[feature.id]" + v-model="checked" /> <label></label> </div> @@ -280,6 +279,15 @@ export default { return arr; }, + checked: { + get() { + return this.checkedFeatures; + }, + set(newChecked) { + this.$store.commit("feature/UPDATE_CHECKED_FEATURES", newChecked); + }, + }, + displayedPageEnd() { return this.filteredFeatures.length <= this.pagination.end ? this.filteredFeatures.length diff --git a/src/components/feature_type/FeatureTypeCustomForm.vue b/src/components/feature_type/FeatureTypeCustomForm.vue index 39d1349eacbe108c3e4679165da14a850583b338..5372073ee270dca4dba201cfdf927b089512c8f8 100644 --- a/src/components/feature_type/FeatureTypeCustomForm.vue +++ b/src/components/feature_type/FeatureTypeCustomForm.vue @@ -106,7 +106,7 @@ v-model="arrayOption" class="options-field" /> - <small>{{ form.help_text }}</small> + <small>{{ form.options.help_text }}</small> <ul id="errorlist" class="errorlist"> <li v-for="error in form.options.errors" :key="error"> {{ error }} @@ -195,7 +195,7 @@ export default { id_for_label: "options", label: "Options", html_name: "options", - help_text: "", + help_text: "Valeurs possibles de ce champ, séparées par des virgules", field: { max_length: 256, }, @@ -284,46 +284,37 @@ export default { }, checkUniqueName() { - console.log(this.$store); - console.log(this.$store.state); - console.log(this.$store.state.feature_type); - if (this.form.name.value) { - const occurences = this.$store.state.feature_type.customForms - .map((el) => el.name) - .filter((el) => el === this.form.name.value); - console.log("occurences", occurences); - console.log(occurences.length); - if (occurences.length > 1) { - console.log("duplicate", this.form.name.value); - this.form.name.errors = [ - "Les champs personnalisés ne peuvent pas avoir des noms similaires.", - ]; - return false; - } - } - this.form.name.errors = []; - return true; + const occurences = this.$store.state.feature_type.customForms + .map((el) => el.name) + .filter((el) => el === this.form.name.value); + return occurences.length === 1; }, checkCustomForm() { - if (this.form.label.value === null) { + this.form.label.errors = []; + this.form.name.errors = []; + if (!this.form.label.value) { + //* vérifier que le label est renseigné this.form.label.errors = ["Veuillez compléter ce champ."]; return false; - } else if (this.form.name.value === null) { + } else if (!this.form.name.value) { + //* vérifier que le nom est renseigné this.form.name.errors = ["Veuillez compléter ce champ."]; - this.form.label.errors = []; return false; } else if (!this.hasRegularCharacters(this.form.name.value)) { + //* vérifier qu'il n'y a pas de caractères spéciaux this.form.name.errors = [ "Veuillez utiliser seulement les caratères autorisés.", ]; - this.form.label.errors = []; return false; - } else if (this.checkUniqueName()) { - this.form.label.errors = []; - this.form.name.errors = []; - return true; + } else if (!this.checkUniqueName()) { + //* vérifier si les noms sont pas dupliqués + this.form.name.errors = [ + "Les champs personnalisés ne peuvent pas avoir des noms similaires.", + ]; + return false; } + return true; }, }, diff --git a/src/store/index.js b/src/store/index.js index 32d4576741a6c5929f78c99ab0771919b8ad2a92..6f543f6d8a48b83ba0b027c7cf7c051f5c4a6a75 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -100,7 +100,7 @@ export default new Vuex.Store({ }, DISPLAY_MESSAGE(state, comment) { state.messages = [{ comment }, ...state.messages]; - document.getElementById("messages").scrollIntoView({ block: "start", inline: "nearest" }); + if (document.getElementById("content")) document.getElementById("content").scrollIntoView({ block: "start", inline: "nearest" }); setTimeout(() => { state.messages = []; }, 3000); diff --git a/src/store/modules/feature.js b/src/store/modules/feature.js index 1f87150b332e56c3a85f3dea923b26fb15ae3a1a..eb6646b5572a64e0d9522d9d31122dc395d63a71 100644 --- a/src/store/modules/feature.js +++ b/src/store/modules/feature.js @@ -14,10 +14,11 @@ const feature = { attachmentFormset: [], attachmentsToDelete: [], attachmentsToPut: [], - linkedFormset: [], + checkedFeatures: [], + extra_form: [], features: [], form: null, - extra_form: [], + linkedFormset: [], linked_features: [], }, mutations: { @@ -79,6 +80,9 @@ const feature = { REMOVE_ATTACHMENTS_ID_TO_DELETE(state, attachementId) { state.attachmentsToDelete = state.attachmentsToDelete.filter(el => el !== attachementId); }, + UPDATE_CHECKED_FEATURES(state, checkedFeatures) { + state.checkedFeatures = checkedFeatures; + } }, getters: { }, diff --git a/src/views/feature/Feature_detail.vue b/src/views/feature/Feature_detail.vue index 62203ff90acf0230451cf02f0f3e22c85579c717..42a6d487f1f1eb874b01ae4d62d4f0a0d451bc98 100644 --- a/src/views/feature/Feature_detail.vue +++ b/src/views/feature/Feature_detail.vue @@ -37,8 +37,9 @@ > <i class="inverted grey pencil alternate icon"></i> </router-link> + <!-- (permissions && permissions.can_delete_feature) || --> <a - v-if="permissions && permissions.can_delete_feature" + v-if="isFeatureCreator" @click="isCanceling = true" id="feature-delete" class="ui button button-hover-red" @@ -101,7 +102,7 @@ v-if="feature.status" :class="getIconLabelStatus(feature.status, 'icon')" ></i> - {{ getIconLabelStatus(feature.status, 'label') }} + {{ getIconLabelStatus(feature.status, "label") }} </td> </tr> <tr> @@ -417,7 +418,6 @@ export default { const result = this.$store.state.feature.features.find( (el) => el.feature_id === this.$route.params.slug_signal ); - console.log("result", result); return result; }, @@ -438,23 +438,19 @@ export default { }, methods: { - getIconLabelStatus(status, type){ - if (status === 'archived') - if (type == 'icon') - return "grey archive icon"; - else return 'Archivé'; - else if (status === 'pending') - if (type == 'icon') - return "teal hourglass outline icon"; - else return 'En attente de publication'; - else if (status === 'published') - if (type == 'icon') - return "olive check icon"; - else return 'Publié'; - else if (status === 'draft') - if (type == 'icon') - return "orange pencil alternate icon"; - else return 'Brouillon'; + getIconLabelStatus(status, type) { + if (status === "archived") + if (type == "icon") return "grey archive icon"; + else return "Archivé"; + else if (status === "pending") + if (type == "icon") return "teal hourglass outline icon"; + else return "En attente de publication"; + else if (status === "published") + if (type == "icon") return "olive check icon"; + else return "Publié"; + else if (status === "draft") + if (type == "icon") return "orange pencil alternate icon"; + else return "Brouillon"; }, pushNgo(link) { this.$router.push({ @@ -533,7 +529,10 @@ export default { .dispatch("feature/DELETE_FEATURE", this.feature.feature_id) .then((response) => { if (response.status === 204) { - this.$store.dispatch("feature/GET_PROJECT_FEATURES", this.$route.params.slug); + this.$store.dispatch( + "feature/GET_PROJECT_FEATURES", + this.$route.params.slug + ); this.goBackToProject(); } }); @@ -599,7 +598,9 @@ export default { if (feature) { const currentFeature = [feature]; const featureGroup = mapUtil.addFeatures(currentFeature); - mapUtil.getMap().fitBounds(featureGroup.getBounds(), { padding: [25, 25] }); + mapUtil + .getMap() + .fitBounds(featureGroup.getBounds(), { padding: [25, 25] }); } }) .catch((error) => { diff --git a/src/views/feature/Feature_edit.vue b/src/views/feature/Feature_edit.vue index 8ea11a374d2b479e2b5f15cd5ff689a56618ae1c..05fba45025c5e32fe6f1cf523512644f042d80c3 100644 --- a/src/views/feature/Feature_edit.vue +++ b/src/views/feature/Feature_edit.vue @@ -74,7 +74,7 @@ <div v-frag v-if="feature_type && feature_type.geom_type === 'point'"> <p> <button - @click="showGeoRef = true" + @click="toggleGeoRefModal" id="add-geo-image" type="button" class="ui compact button" @@ -84,44 +84,69 @@ Vous pouvez utiliser une image géoréférencée pour localiser le signalement. </p> - <div v-if="showGeoRef"> - <p> - Attention, si vous avez déjà saisi une géométrie, celle issue de - l'image importée l'écrasera. - </p> - <div class="field georef-btn"> - <label>Image (png ou jpeg)</label> - <label class="ui icon button" for="image_file"> - <i class="file icon"></i> - <span class="label">{{ geoRefFileLabel }}</span> - </label> - <input - type="file" - accept="image/jpeg, image/png" - style="display: none" - ref="file" - v-on:change="handleFileUpload()" - name="image_file" - class="image_file" - id="image_file" - /> - <p class="error-message" style="color: red"> - {{ erreurUploadMessage }} - </p> - </div> - <button - @click="georeferencement()" - id="get-geom-from-image-file" - type="button" - class="ui positive right labeled icon button" + + <div + v-if="showGeoRef" + class="ui dimmer modals page transition visible active" + style="display: flex !important" + > + <div + class="ui mini modal transition visible active" + style="display: block !important" > - Importer - <i class="checkmark icon"></i> - </button> + <i class="close icon" @click="toggleGeoRefModal"></i> + <div class="content"> + <h3>Importer une image géoréférencée</h3> + <form + id="form-geo-image" + class="ui form" + enctype="multipart/form-data" + > + <p> + Attention, si vous avez déjà saisi une géométrie, celle + issue de l'image importée l'écrasera. + </p> + <div class="field georef-btn"> + <label>Image (png ou jpeg)</label> + <label class="ui icon button" for="image_file"> + <i class="file icon"></i> + <span class="label">{{ geoRefFileLabel }}</span> + </label> + <input + type="file" + accept="image/jpeg, image/png" + style="display: none" + ref="file" + v-on:change="handleFileUpload" + name="image_file" + class="image_file" + id="image_file" + /> + <p class="error-message" style="color: red"> + {{ erreurUploadMessage }} + </p> + </div> + <button + @click="georeferencement" + id="get-geom-from-image-file" + type="button" + :class="[ + 'ui compact button', + file && !erreurUploadMessage ? 'green' : 'disabled', + { red: erreurUploadMessage }, + ]" + > + <i class="plus icon"></i> + Importer + </button> + </form> + </div> + </div> </div> + <p v-if="showGeoPositionBtn"> <button - @click="create_point_geoposition()" + @click="create_point_geoposition" id="create-point-geoposition" type="button" class="ui compact button" @@ -173,7 +198,7 @@ <!-- Extra Fields --> <div class="ui horizontal divider">DONNÉES MÉTIER</div> <div - v-for="(field, index) in extra_form" + v-for="(field, index) in orderedCustomFields" :key="field.field_type + index" class="field" > @@ -367,6 +392,12 @@ export default { ); }, + orderedCustomFields() { + return [...this.extra_form].sort( + (a, b) => a.position - b.position + ); + }, + geoRefFileLabel() { if (this.file) { return this.file.name; @@ -458,6 +489,10 @@ export default { function error(err) { this.erreurGeolocalisationMessage = err.message; + if (err.message === "User denied geolocation prompt") { + this.erreurGeolocalisationMessage = + "La géolocalisation a été désactivé par l'utilisateur"; + } } this.erreurGeolocalisationMessage = null; if (!navigator.geolocation) { @@ -471,7 +506,17 @@ export default { } }, + toggleGeoRefModal() { + if (this.showGeoRef) { + //* when popup closes, empty form + this.erreurUploadMessage = ""; + this.file = null; + } + this.showGeoRef = !this.showGeoRef; + }, + handleFileUpload() { + this.erreurUploadMessage = ""; this.file = this.$refs.file.files[0]; }, @@ -481,19 +526,17 @@ export default { let formData = new FormData(); formData.append("image_file", this.file); console.log(">> formData >> ", formData); - let self = this; axios .post(url, formData, { headers: { "Content-Type": "multipart/form-data", }, }) - .then(function (response) { + .then((response) => { console.log("SUCCESS!!", response.data); if (response.data.geom.indexOf("POINT") >= 0) { let regexp = /POINT\s\((.*)\s(.*)\)/; - var arr = regexp.exec(response.data.geom); - + let arr = regexp.exec(response.data.geom); let json = { type: "Feature", geometry: { @@ -502,21 +545,21 @@ export default { }, properties: {}, }; - self.updateMap(json); - self.updateGeomField(json); + this.updateMap(json); + this.updateGeomField(json); // Set Attachment - self.addAttachment({ + this.addAttachment({ title: "Localisation", info: "", id: "loc", - attachment_file: self.file.name, - fileToImport: self.file, + attachment_file: this.file.name, + fileToImport: this.file, }); } }) - .catch(function (response) { + .catch((response) => { console.log("FAILURE!!"); - self.erreurUploadMessage = response.data.message; + this.erreurUploadMessage = response.data.message; }); }, @@ -956,7 +999,7 @@ export default { this.initForm(); this.initMap(); this.onFeatureTypeLoaded(); - this.initExtraForms(); + this.initExtraForms(this.feature); setTimeout( function () { @@ -1002,4 +1045,9 @@ export default { .ui.segment { margin: 1rem 0 !important; } +/* override to display buttons under the dimmer of modal */ +.leaflet-top, +.leaflet-bottom { + z-index: 800; +} </style> diff --git a/src/views/feature/Feature_list.vue b/src/views/feature/Feature_list.vue index 9c148830cfcbae757bf028edd70a1caa1cf04cd0..f5d4b5ba5abd4560312d060de958d326739424b2 100644 --- a/src/views/feature/Feature_list.vue +++ b/src/views/feature/Feature_list.vue @@ -198,7 +198,6 @@ export default { data() { return { modalAllDeleteOpen: false, - checkedFeatures: [], form: { type: { selected: null, @@ -247,7 +246,7 @@ export default { computed: { ...mapGetters(["project", "permissions"]), ...mapState(["user"]), - ...mapState("feature", ["features"]), + ...mapState("feature", ["features", "checkedFeatures"]), ...mapState("feature_type", ["feature_types"]), baseMaps() { diff --git a/src/views/feature_type/Feature_type_detail.vue b/src/views/feature_type/Feature_type_detail.vue index 37a732a06ff1fba6c92e8eeb21af894c752b39a8..e0dbf923db86d99d0daf3ae7b35f62c5396948d3 100644 --- a/src/views/feature_type/Feature_type_detail.vue +++ b/src/views/feature_type/Feature_type_detail.vue @@ -35,7 +35,7 @@ <h3 class="ui header">Champs</h3> <div class="ui divided list"> <div - v-for="(field, index) in structure.customfield_set" + v-for="(field, index) in orderedCustomFields" :key="field.name + index" class="item" > @@ -154,10 +154,10 @@ }} </div> <div> - Créé le {{ feature.created_on }} - <span v-if="$store.state.user.is_authenticated"> + [ Créé le {{ feature.created_on | formatDate }} + <span v-if="$store.state.user"> par {{ feature.display_creator }}</span - > + > ] </div> </div> </div> @@ -204,6 +204,14 @@ export default { }; }, + filters: { + formatDate(value) { + let date = new Date(value); + date = date.toLocaleString().replace(",", " à "); + return date.substr(0, date.length - 3); //* quick & dirty way to remove seconds from date + }, + }, + computed: { ...mapGetters(["project", "permissions"]), ...mapState("feature", ["features"]), @@ -226,6 +234,12 @@ export default { lastFeatures: function () { return this.feature_type_features.slice(0, 5); }, + + orderedCustomFields() { + return [...this.structure.customfield_set].sort( + (a, b) => a.position - b.position + ); + }, }, methods: { diff --git a/src/views/feature_type/Feature_type_edit.vue b/src/views/feature_type/Feature_type_edit.vue index 691443a6d7d5ce34aa95be04ca40851e3e536a85..8786d6d25fd55758885d54420e66447efed1845d 100644 --- a/src/views/feature_type/Feature_type_edit.vue +++ b/src/views/feature_type/Feature_type_edit.vue @@ -153,7 +153,6 @@ <i class="white save icon"></i> Créer et importer le(s) signalement(s) du geojson </button> - </form> </div> </div> @@ -345,13 +344,14 @@ export default { }, checkCustomForms() { + let is_valid = true; if (this.$refs.customForms) for (const customForm of this.$refs.customForms) { if (customForm.checkCustomForm() === false) { - return false; + is_valid = false; } } - return true; //* fallback if all customForms returned true + return is_valid; //* fallback if all customForms returned true }, checkForms() {