diff --git a/src/App.vue b/src/App.vue index 343ccfe54930b931fc6bc1111399d9ee6d749a6a..940e4ea9c7ee80e27d188c66d6a644d4be5a1ef1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -151,6 +151,11 @@ </div> </div> </div> + <div :class="{ active: loader.isLoading }" class="ui inverted dimmer"> + <div class="ui text loader"> + {{ loader.message }} + </div> + </div> <router-view /> <!-- //* Les views sont injectées ici --> </div> @@ -194,6 +199,7 @@ export default { "USER_LEVEL_PROJECTS", "configuration", "messages", + "loader", ]), ...mapGetters(["project"]), APPLICATION_NAME: function () { @@ -256,6 +262,11 @@ body { flex-direction: column; } +/* to display loader between header and footer */ +main { + position: relative; +} + footer { margin-top: auto; } @@ -269,28 +280,32 @@ footer { background: white !important; } +.flex { + display: flex; +} + +/* keep above loader */ +#menu-dropdown { + z-index: 1001; +} + @media screen and (min-width: 560px) { .mobile { display: none !important; } - .header-menu { min-width: 560px; } - .menu.container { width: auto !important; margin-left: 1em !important; margin-right: 1em !important; } - .push-right-desktop { margin-left: auto; } } -.flex { - display: flex; -} + @media screen and (max-width: 560px) { .desktop { display: none !important; 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 eef57b2b52cbdccab9fc2c740888b4b501b446f8..6f543f6d8a48b83ba0b027c7cf7c051f5c4a6a75 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -48,12 +48,11 @@ export default new Vuex.Store({ USER_LEVEL_PROJECTS: null, user_permissions: null, messages: [], - events: null - // events: { - // 'events': null, - // 'features': null, - // 'comments': null - // } + events: null, + loader: { + isLoading: false, + message: "En cours de chargement" + }, }, mutations: { @@ -101,14 +100,23 @@ 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); }, CLEAR_MESSAGES(state) { state.messages = []; - } + }, + DISPLAY_LOADER(state, message) { + state.loader = { isLoading: true, message } + }, + DISCARD_LOADER(state) { + state.loader = { + isLoading: false, + message: "En cours de chargement" + }; + }, }, getters: { diff --git a/src/store/modules/feature.js b/src/store/modules/feature.js index 3e248934f355806b289669ced8beaf24348457d8..9b629c72ca3f301918d20e238778fe29739d02e9 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: [], statusChoices: [ { @@ -97,6 +98,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: { }, @@ -117,9 +121,12 @@ const feature = { }); }, - SEND_FEATURE({ state, rootState, dispatch }, routeName) { + SEND_FEATURE({ state, rootState, commit, dispatch }, routeName) { + commit("DISPLAY_LOADER", "Le signalement est en cours de création", { root: true }) const message = routeName === "editer-signalement" ? "Le signalement a été mis à jour" : "Le signalement a été crée"; + function redirect(featureId) { + commit("DISCARD_LOADER", null, { root: true }) router.push({ name: "details-signalement", params: { @@ -129,12 +136,14 @@ const feature = { }, }); } + async function handleOtherForms(featureId) { await dispatch("SEND_ATTACHMENTS", featureId) await dispatch("PUT_LINKED_FEATURES", featureId) redirect(featureId); } + //* prepare feature data to send let extraFormObject = {}; //* prepare an object to be flatten in properties of geojson for (const field of state.extra_form) { extraFormObject[field.name] = field.value; @@ -152,8 +161,9 @@ const feature = { ...extraFormObject } } + if (routeName === "editer-signalement") { - axios + return axios .put(`${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/${state.form.feature_id}/?` + `feature_type__slug=${rootState.feature_type.current_feature_type_slug}` + `&project__slug=${rootState.project_slug}` @@ -168,10 +178,11 @@ const feature = { } }) .catch((error) => { + commit("DISCARD_LOADER", null, { root: true }) throw error; }); } else { - axios + return axios .post(`${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/`, geojson) .then((response) => { if (response.status === 201 && response.data) { @@ -183,6 +194,7 @@ const feature = { } }) .catch((error) => { + commit("DISCARD_LOADER", null, { root: true }) throw error; }); } diff --git a/src/views/feature/Feature_detail.vue b/src/views/feature/Feature_detail.vue index 2a2951a8d3af652b8ac345312e54333aca278b37..5d929e5b58ada4df68fcd81071f589d9cb4ac30b 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" diff --git a/src/views/feature/Feature_edit.vue b/src/views/feature/Feature_edit.vue index c948a71abe0305befdb0530801dd065422f99a9d..e18201375c735d01e37870afb4c5c2d8aa705f09 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" > @@ -350,6 +375,12 @@ export default { ); }, + orderedCustomFields() { + return [...this.extra_form].sort( + (a, b) => a.position - b.position + ); + }, + geoRefFileLabel() { if (this.file) { return this.file.name; @@ -441,6 +472,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) { @@ -454,7 +489,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]; }, @@ -464,19 +509,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: { @@ -485,21 +528,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; }); }, @@ -939,7 +982,7 @@ export default { this.initForm(); this.initMap(); this.onFeatureTypeLoaded(); - this.initExtraForms(); + this.initExtraForms(this.feature); setTimeout( function () { @@ -985,4 +1028,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 79b7f1342d4563e29850e5e7793f52d0eaa0c0b7..e3906d9f6020911433cbd6fcf232f1cd49506ce5 100644 --- a/src/views/feature/Feature_list.vue +++ b/src/views/feature/Feature_list.vue @@ -1,6 +1,5 @@ <template> <div class="fourteen wide column"> - <div class="ui dimmer" :class="[{ active: featureLoading }]"></div> <script type="application/javascript" :src=" @@ -38,7 +37,11 @@ </div> <div - v-if="project && feature_types && permissions.can_create_feature" + v-if=" + project && + feature_types.length > 0 && + permissions.can_create_feature + " class="item right" > <div @@ -88,7 +91,6 @@ <div class="field wide four column no-margin-mobile"> <label>Type</label> <Dropdown - @update:selection="onFilterTypeChange($event)" :options="form.type.choices" :selected="form.type.selected" :selection.sync="form.type.selected" @@ -100,8 +102,7 @@ <label>Statut</label> <!-- //* giving an object mapped on key name --> <Dropdown - @update:selection="onFilterStatusChange($event)" - :options="form.status.choices" + :options="statusChoices" :selected="form.status.selected.name" :selection.sync="form.status.selected" :search="true" @@ -117,7 +118,7 @@ type="text" name="title" v-model="form.title" - @input="onFilterChange()" + @input="onFilterChange" /> <button type="button" @@ -129,7 +130,7 @@ </div> </div> </div> - <!-- map params, updated on map move // todo : brancher sur la carte probablement --> + <!-- map params, updated on map move --> <input type="hidden" name="zoom" v-model="zoom" /> <input type="hidden" name="lat" v-model="lat" /> <input type="hidden" name="lng" v-model="lng" /> @@ -199,7 +200,6 @@ export default { data() { return { modalAllDeleteOpen: false, - checkedFeatures: [], form: { type: { selected: null, @@ -233,7 +233,6 @@ export default { }, geojsonFeatures: [], - featureLoading: false, filterStatus: null, filterType: null, baseUrl: this.$store.state.configuration.BASE_URL, @@ -249,7 +248,7 @@ export default { computed: { ...mapGetters(["project", "permissions"]), ...mapState(["user"]), - ...mapState("feature", ["features"]), + ...mapState("feature", ["features", "checkedFeatures"]), ...mapState("feature_type", ["feature_types"]), baseMaps() { @@ -258,15 +257,15 @@ export default { filteredFeatures() { let results = this.geojsonFeatures; - if (this.filterType) { + if (this.form.type.selected) { results = results.filter( - (el) => el.properties.feature_type.title === this.filterType + (el) => el.properties.feature_type.title === this.form.type.selected ); } - if (this.filterStatus) { - console.log("filter by" + this.filterStatus); + if (this.form.status.selected.value) { + console.log("filter by" + this.form.status.selected.value); results = results.filter( - (el) => el.properties.status.value === this.filterStatus + (el) => el.properties.status.value === this.form.status.selected.value ); } if (this.form.title) { @@ -281,6 +280,21 @@ export default { } return results; }, + + statusChoices() { + //* if project is not moderate, remove pending status + return this.form.status.choices.filter((el) => + this.project.moderation ? true : el.value !== "pending" + ); + }, + }, + + watch: { + filteredFeatures(newValue, oldValue) { + if (newValue && newValue !== oldValue) { + this.onFilterChange() + } + } }, methods: { @@ -311,30 +325,11 @@ export default { this.modalAllDelete(); }, - onFilterStatusChange(newvalue) { - this.filterStatus = null; - if (newvalue) { - console.log("filter change", newvalue.value); - this.filterStatus = newvalue.value; - } - - this.onFilterChange(); - }, - - onFilterTypeChange(newvalue) { - this.filterType = null; - if (newvalue) { - console.log("filter change", newvalue); - this.filterType = newvalue; - } - this.onFilterChange(); - }, - onFilterChange() { - var features = this.filteredFeatures; - this.featureGroup.clearLayers(); - this.featureGroup = mapUtil.addFeatures(features, {}); - if (features.length > 0) { + if (this.featureGroup) { + const features = this.filteredFeatures; + this.featureGroup.clearLayers(); + this.featureGroup = mapUtil.addFeatures(features, {}); mapUtil .getMap() .fitBounds(this.featureGroup.getBounds(), { padding: [25, 25] }); @@ -365,19 +360,23 @@ export default { }); // --------- End sidebar events ---------- - if (this.$store.state.map.geojsonFeatures) { - this.loadFeatures(this.$store.state.map.geojsonFeatures); - } else { + if (this.features && this.features.length > 0) { + //* features are updated consistently, then if features exists, we can fetch the geojson version const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/?output=geojson`; - this.featureLoading = true; + this.$store.commit( + "DISPLAY_LOADER", + "Récupération des signalements en cours..." + ); axios .get(url) .then((response) => { - this.loadFeatures(response.data.features); - this.featureLoading = false; + if (response.status === 200 && response.data.features.length > 0) { + this.loadFeatures(response.data.features); + } + this.$store.commit("DISCARD_LOADER"); }) .catch((error) => { - this.featureLoading = false; + this.$store.commit("DISCARD_LOADER"); throw error; }); } @@ -429,6 +428,11 @@ export default { mounted() { this.initMap(); }, + + destroyed() { + //* allow user to change page if ever stuck on loader + this.$store.commit("DISCARD_LOADER"); + }, }; </script> 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() { diff --git a/src/views/project/Project_detail.vue b/src/views/project/Project_detail.vue index 5087954315d796ba8161c98966784ce467d44505..b77188127e9e778f1f8b306569f250629617dff8 100644 --- a/src/views/project/Project_detail.vue +++ b/src/views/project/Project_detail.vue @@ -590,7 +590,6 @@ export default { if (this.project && this.permissions.can_view_project) { this.$store.dispatch("map/INITIATE_MAP"); const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/?output=geojson`; - let self = this; axios .get(url) .then((response) => { @@ -600,7 +599,9 @@ export default { mapUtil .getMap() .fitBounds(featureGroup.getBounds(), { padding: [25, 25] }); - self.$store.commit("map/SET_GEOJSON_FEATURES", features); + this.$store.commit("map/SET_GEOJSON_FEATURES", features); + } else { + this.$store.commit("map/SET_GEOJSON_FEATURES", []); } }) .catch((error) => { diff --git a/src/views/project/Project_members.vue b/src/views/project/Project_members.vue index a838d755e80c0131efa9de35a3b5b1c63955eea9..dd1fc1c606342a8a6240da3d574f75852a5dca56 100644 --- a/src/views/project/Project_members.vue +++ b/src/views/project/Project_members.vue @@ -132,7 +132,12 @@ export default { }, async populateMembers() { + this.$store.commit( + "DISPLAY_LOADER", + "Récupération des membres en cours..." + ); await this.fetchMembers().then((members) => { + this.$store.commit("DISCARD_LOADER"); this.projectMembers = members.map((el) => { return { userLevel: { name: el.level.display, value: el.level.codename }, @@ -149,5 +154,10 @@ export default { } this.populateMembers(); }, + + destroyed() { + //* allow user to change page if ever stuck on loader + this.$store.commit("DISCARD_LOADER"); + }, }; </script> \ No newline at end of file