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 974ee922ebc6016d1a10d1841575d1ae13adb989..67079e83ac176f0bbc48a085eb788aef6630ef37 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: { }, @@ -137,9 +141,9 @@ const feature = { if (routeName === "editer-signalement") { 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}` - , geojson) + `feature_type__slug=${rootState.feature_type.current_feature_type_slug}` + + `&project__slug=${rootState.project_slug}` + , geojson) .then((response) => { if (response.status === 200 && response.data) { if (state.attachmentFormset.length > 0 || state.linkedFormset.length > 0) { @@ -189,7 +193,7 @@ const feature = { console.error(error); return error }); - + } } @@ -203,7 +207,7 @@ const feature = { if (attachment.title) data['info'] = attachment.info formdataToUpdate.append("data", JSON.stringify(data)); - let payload ={ + let payload = { 'attachmentsId': attachment.id, 'featureId': featureId, 'formdataToUpdate': formdataToUpdate @@ -213,7 +217,7 @@ const feature = { } function deleteAttachement(attachmentsId, featureId) { - let payload ={ + let payload = { 'attachmentsId': attachmentsId, 'featureId': featureId } @@ -224,7 +228,7 @@ const feature = { ...state.attachmentFormset.map((attachment) => postAttachement(attachment)), ...state.attachmentsToPut.map((attachments) => putAttachement(attachments, featureId)), ...state.attachmentsToDelete.map((attachmentsId) => deleteAttachement(attachmentsId, featureId)) - ] + ] ); state.attachmentsToDelete = [] state.attachmentsToPut = [] @@ -280,8 +284,8 @@ const feature = { DELETE_FEATURE({ state, rootState }, feature_id) { console.log("Deleting feature:", feature_id, state) const url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/${feature_id}/?` + - `feature_type__slug=${rootState.feature_type.current_feature_type_slug}` + - `&project__slug=${rootState.project_slug}`; + `feature_type__slug=${rootState.feature_type.current_feature_type_slug}` + + `&project__slug=${rootState.project_slug}`; return axios .delete(url) .then((response) => response) 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 985096d769e26bf34bd5d4a86e0a59245159dc90..978f73da054d5e111c1b53f7e66ba0ce044a00d1 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,41 +84,66 @@ 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"> - {{ 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"> + {{ 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" @@ -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,10 @@ export default { ); }, + orderedCustomFields() { + return [...this.extra_form].sort((a, b) => a.position - b.position); + }, + geoRefFileLabel() { if (this.file) { return this.file.name; @@ -458,6 +487,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 +504,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 +524,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,25 +543,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 (error) { - if (error && error.response && error.response) { - self.erreurUploadMessage = error.response.data.error; - } else { - self.erreurUploadMessage = - "Une erreur est survenue pendant l'import de l'image géoréférencée"; - } + .catch((response) => { + console.log("FAILURE!!"); + this.erreurUploadMessage = response.data.message; }); }, @@ -960,7 +997,7 @@ export default { this.initForm(); this.initMap(); this.onFeatureTypeLoaded(); - this.initExtraForms(); + this.initExtraForms(this.feature); setTimeout( function () { @@ -1010,4 +1047,9 @@ export default { .error-message { color: red; } +/* 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..f5d4b5ba5abd4560312d060de958d326739424b2 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=" @@ -199,7 +198,6 @@ export default { data() { return { modalAllDeleteOpen: false, - checkedFeatures: [], form: { type: { selected: null, @@ -233,7 +231,6 @@ export default { }, geojsonFeatures: [], - featureLoading: false, filterStatus: null, filterType: null, baseUrl: this.$store.state.configuration.BASE_URL, @@ -249,7 +246,7 @@ export default { computed: { ...mapGetters(["project", "permissions"]), ...mapState(["user"]), - ...mapState("feature", ["features"]), + ...mapState("feature", ["features", "checkedFeatures"]), ...mapState("feature_type", ["feature_types"]), baseMaps() { @@ -369,15 +366,18 @@ export default { this.loadFeatures(this.$store.state.map.geojsonFeatures); } else { 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; + this.$store.commit("DISCARD_LOADER"); }) .catch((error) => { - this.featureLoading = false; + this.$store.commit("DISCARD_LOADER"); throw error; }); } @@ -429,6 +429,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_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