Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • geocontrib/geocontrib-frontend
  • ext_matthieu/geocontrib-frontend
  • fnecas/geocontrib-frontend
  • MatthieuE/geocontrib-frontend
4 results
Show changes
Commits on Source (35)
Showing
with 1251 additions and 626 deletions
{ {
"name": "geocontrib-frontend", "name": "geocontrib-frontend",
"version": "3.0.1", "version": "3.0.2",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "npm run init-proxy & npm run init-serve", "serve": "npm run init-proxy & npm run init-serve",
...@@ -16,7 +16,9 @@ ...@@ -16,7 +16,9 @@
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^2.0.6", "@fortawesome/vue-fontawesome": "^2.0.6",
"@mapbox/vector-tile": "^1.3.1", "@mapbox/vector-tile": "^1.3.1",
"@turf/bbox": "^6.5.0",
"@turf/flip": "^6.5.0", "@turf/flip": "^6.5.0",
"@turf/helpers": "^6.5.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"core-js": "^3.20.2", "core-js": "^3.20.2",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
...@@ -33,13 +35,13 @@ ...@@ -33,13 +35,13 @@
"vuex": "^3.6.2" "vuex": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.15.8",
"@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-free": "^5.15.4",
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "^5.0.0-beta.6", "@vue/cli-plugin-eslint": "^5.0.0-beta.6",
"@vue/cli-plugin-pwa": "~4.5.0", "@vue/cli-plugin-pwa": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0", "@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-service": "~4.5.0", "@vue/cli-service": "~4.5.0",
"@babel/eslint-parser": "^7.15.8",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-vue": "^7.20.0", "eslint-plugin-vue": "^7.20.0",
"less": "^3.0.4", "less": "^3.0.4",
......
...@@ -141,7 +141,7 @@ ...@@ -141,7 +141,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="desktop flex push-right-desktop item title"> <div class="desktop flex push-right-desktop item title abstract">
<span> <span>
{{ APPLICATION_ABSTRACT }} {{ APPLICATION_ABSTRACT }}
</span> </span>
...@@ -405,7 +405,13 @@ footer { ...@@ -405,7 +405,13 @@ footer {
z-index: 1001; z-index: 1001;
} }
@media screen and (min-width: 560px) { @media screen and (max-width: 985px) {
.abstract{
display: none !important;
}
}
@media screen and (min-width: 590px) {
.mobile { .mobile {
display: none !important; display: none !important;
} }
...@@ -420,7 +426,7 @@ footer { ...@@ -420,7 +426,7 @@ footer {
} }
} }
@media screen and (max-width: 560px) { @media screen and (max-width: 590px) {
.desktop { .desktop {
display: none !important; display: none !important;
} }
......
...@@ -105,9 +105,9 @@ L.TileLayer.BetterWMS = L.TileLayer.WMS.extend({ ...@@ -105,9 +105,9 @@ L.TileLayer.BetterWMS = L.TileLayer.WMS.extend({
if (err) { if (err) {
content = ` content = `
<h4>${this.options.title}</h4> <h4>${this.options.title}</h4>
<p>Données de la couche inaccessibles</p> <p>Données de la couche inaccessibles</p>
`; `;
L.popup({ maxWidth: 800 }) L.popup({ maxWidth: 800 })
.setLatLng(latlng) .setLatLng(latlng)
...@@ -156,15 +156,14 @@ const mapUtil = { ...@@ -156,15 +156,14 @@ const mapUtil = {
zoom, zoom,
zoomControl = true, zoomControl = true,
} = options; } = options;
map = L.map(el, { map = L.map(el, {
maxZoom: 18, maxZoom: 18,
minZoom: 1, minZoom: 1,
zoomControl: false, zoomControl: false,
}).setView( }).setView(
[ [
!lat ? mapDefaultViewCenter[0] : lat, lat ? lat : mapDefaultViewCenter[0],
!lng ? mapDefaultViewCenter[1] : lng, lng ? lng : mapDefaultViewCenter[1],
], ],
!zoom ? mapDefaultViewZoom : zoom !zoom ? mapDefaultViewZoom : zoom
); );
...@@ -295,9 +294,8 @@ const mapUtil = { ...@@ -295,9 +294,8 @@ const mapUtil = {
const currentValue = properties[colorsStyle.custom_field_name]; const currentValue = properties[colorsStyle.custom_field_name];
const colorStyle = colorsStyle.colors[currentValue]; const colorStyle = colorsStyle.colors[currentValue];
return colorStyle ? colorStyle : featureType.color; return colorStyle ? colorStyle : featureType.color;
} else {
return featureType.color;
} }
return featureType.color;
}, },
addVectorTileLayer: function (url, project_slug, featureTypes, form_filters) { addVectorTileLayer: function (url, project_slug, featureTypes, form_filters) {
...@@ -370,7 +368,7 @@ const mapUtil = { ...@@ -370,7 +368,7 @@ const mapUtil = {
window.layerMVT = layerMVT; window.layerMVT = layerMVT;
}, },
addFeatures: function (features, filter, addToMap = true, featureTypes) { addFeatures: function (features, filter, addToMap = true, featureTypes, project_slug) {
const featureGroup = new L.FeatureGroup(); const featureGroup = new L.FeatureGroup();
features.forEach((feature) => { features.forEach((feature) => {
const featureProperties = feature.properties ? feature.properties : feature; const featureProperties = feature.properties ? feature.properties : feature;
...@@ -391,7 +389,7 @@ const mapUtil = { ...@@ -391,7 +389,7 @@ const mapUtil = {
filters.length && filters.every(val => val !== false) filters.length && filters.every(val => val !== false)
) { ) {
const geomJSON = flip(feature.geometry || feature.geom); const geomJSON = flip(feature.geometry || feature.geom);
const popupContent = this._createContentPopup(feature); const popupContent = this._createContentPopup(feature, featureTypes, project_slug);
// Look for a custom field // Look for a custom field
let customField; let customField;
...@@ -404,14 +402,16 @@ const mapUtil = { ...@@ -404,14 +402,16 @@ const mapUtil = {
.filter(el => featureType.customfield_set.map(e => e.name).includes(el)); .filter(el => featureType.customfield_set.map(e => e.name).includes(el));
customFieldOption = featureProperties[customField[0]]; customFieldOption = featureProperties[customField[0]];
} }
let color = this.retrieveFeatureColor(featureType, featureProperties) || featureProperties.color; let color = '#000000';
if (color == undefined){ if (feature.overideColor) {
color = featureType.color; color = feature.overideColor;
} else {
color = this.retrieveFeatureColor(featureType, featureProperties) || featureProperties.color;
if (color.value && color.value.length) {
color = color.value;
}
} }
const colorValue =
color.value && color.value.length ?
color.value : typeof color === 'string' && color.length ?
color : '#000000';
if (geomJSON.type === 'Point') { if (geomJSON.type === 'Point') {
if ( if (
customFieldOption && customFieldOption &&
...@@ -427,7 +427,7 @@ const mapUtil = { ...@@ -427,7 +427,7 @@ const mapUtil = {
const iconHTML = ` const iconHTML = `
<i <i
class="fas fa-${featureType.colors_style.value.icons[customFieldOption]} fa-lg" class="fas fa-${featureType.colors_style.value.icons[customFieldOption]} fa-lg"
style="color: ${colorValue}" style="color: ${color}"
></i> ></i>
`; `;
const customMapIcon = L.divIcon({ const customMapIcon = L.divIcon({
...@@ -437,14 +437,14 @@ const mapUtil = { ...@@ -437,14 +437,14 @@ const mapUtil = {
}); });
L.marker(geomJSON.coordinates, { L.marker(geomJSON.coordinates, {
icon: customMapIcon, icon: customMapIcon,
color: colorValue, color: color,
zIndexOffset: 100 zIndexOffset: 100
}) })
.bindPopup(popupContent) .bindPopup(popupContent)
.addTo(featureGroup); .addTo(featureGroup);
} else { } else {
L.circleMarker(geomJSON.coordinates, { L.circleMarker(geomJSON.coordinates, {
color: colorValue, color: color,
radius: 4, radius: 4,
fillOpacity: 0.5, fillOpacity: 0.5,
weight: 3, weight: 3,
...@@ -457,7 +457,7 @@ const mapUtil = { ...@@ -457,7 +457,7 @@ const mapUtil = {
const iconHTML = ` const iconHTML = `
<i <i
class="fas fa-${featureType.icon} fa-lg" class="fas fa-${featureType.icon} fa-lg"
style="color: ${colorValue}" style="color: ${color}"
></i> ></i>
`; `;
const customMapIcon = L.divIcon({ const customMapIcon = L.divIcon({
...@@ -467,7 +467,7 @@ const mapUtil = { ...@@ -467,7 +467,7 @@ const mapUtil = {
}); });
L.marker(geomJSON.coordinates, { L.marker(geomJSON.coordinates, {
icon: customMapIcon, icon: customMapIcon,
color: colorValue, color: color,
zIndexOffset: 100 zIndexOffset: 100
}) })
.bindPopup(popupContent) .bindPopup(popupContent)
...@@ -485,14 +485,14 @@ const mapUtil = { ...@@ -485,14 +485,14 @@ const mapUtil = {
} }
} else if (geomJSON.type === 'LineString') { } else if (geomJSON.type === 'LineString') {
L.polyline(geomJSON.coordinates, { L.polyline(geomJSON.coordinates, {
color: colorValue, color: color,
weight: 3, weight: 3,
}) })
.bindPopup(popupContent) .bindPopup(popupContent)
.addTo(featureGroup); .addTo(featureGroup);
} else if (geomJSON.type === 'Polygon') { } else if (geomJSON.type === 'Polygon') {
L.polygon(geomJSON.coordinates, { L.polygon(geomJSON.coordinates, {
color: colorValue, color: color,
weight: 3, weight: 3,
fillOpacity: 0.5, fillOpacity: 0.5,
}) })
...@@ -501,6 +501,7 @@ const mapUtil = { ...@@ -501,6 +501,7 @@ const mapUtil = {
} }
} }
}); });
if (map && addToMap) { if (map && addToMap) {
map.addLayer(featureGroup); map.addLayer(featureGroup);
} }
...@@ -517,32 +518,26 @@ const mapUtil = { ...@@ -517,32 +518,26 @@ const mapUtil = {
('0' + current_datetime.getHours()).slice(-2) + ':' + ('0' + current_datetime.getMinutes()).slice(-2); ('0' + current_datetime.getHours()).slice(-2) + ':' + ('0' + current_datetime.getMinutes()).slice(-2);
return formatted_date; return formatted_date;
}; };
let feature_type; let feature_type = feature.properties ? feature.properties.feature_type : feature.feature_type;
let status; let feature_url = feature.feature_url;
let date_maj; let status = feature.status;
let feature_type_url; let feature_type_url = feature.feature_type_url;
let feature_url; let date_maj = feature.updated_on;
if (feature.properties) { if (feature.properties) {
status = feature.properties.status; status = feature.properties.status;
date_maj = feature.properties.updated_on; date_maj = feature.properties.updated_on ? formatDate(new Date(feature.properties.updated_on)) : '<i>indisponible</i>';
feature_type_url = feature.properties.feature_type_url; feature_type_url = feature.properties.feature_type_url;
feature_url = feature.properties.feature_url; feature_url = feature.properties.feature_url;
} else {
status = feature.status;
date_maj = feature.updated_on;
feature_type_url =feature.feature_type_url;
feature_url = feature.feature_url;
} }
if (featureTypes) { // => VectorTile if (featureTypes && feature.properties) { // => VectorTile
feature_type = featureTypes.find((x) => x.slug.split('-')[0] === '' + feature.properties.feature_type_id); feature_type = feature.properties.feature_type_id ?
featureTypes.find((x) => x.slug.split('-')[0] === '' + feature.properties.feature_type_id) :
featureTypes.find((f_type) => f_type.slug === feature.properties.feature_type); //* geojson
status = statusList.find((x) => x.value === feature.properties.status).name; status = statusList.find((x) => x.value === feature.properties.status).name;
date_maj = formatDate(new Date(feature.properties.updated_on)); if (feature_type) feature_type_url = '/geocontrib/projet/' + project_slug + '/type-signalement/' + feature_type.slug + '/';
feature_type_url = '/geocontrib/projet/' + project_slug + '/type-signalement/' + feature_type.slug + '/';
feature_url = feature_type_url + 'signalement/' + feature.properties.feature_id + '/'; feature_url = feature_type_url + 'signalement/' + feature.properties.feature_id + '/';
} else { } else {
feature_type = feature.properties ? feature.properties.feature_type : feature.feature_type;
status = feature.properties ? feature.properties.status.label : feature.status.label; status = feature.properties ? feature.properties.status.label : feature.status.label;
} }
...@@ -556,27 +551,37 @@ const mapUtil = { ...@@ -556,27 +551,37 @@ const mapUtil = {
if (creator) { if (creator) {
author = creator.full_name author = creator.full_name
? `<div> ? `<div>
Auteur : ${creator.first_name} ${creator.last_name} Auteur : ${creator.first_name} ${creator.last_name}
</div>` </div>`
: creator.username ? `<div>Auteur: ${creator.username}</div>` : ''; : creator.username ? `<div>Auteur: ${creator.username}</div>` : '';
} }
const title = feature.properties ? feature.properties.title : feature.title; let title = feature.properties ? feature.properties.title : feature.title;
if (feature_url) {
title = `<a href="${feature_url}">${title}</a>`;
} else {
title = `<span>${title|| '<i>indisponible</i>'}</span>`;
}
if (feature_type_url) {
`<a href="${feature_type_url}"> ${feature_type.title || '<i>indisponible</i>'} </a>`;
}
return ` return `
<h4> <h4>
<a href="${feature_url}">${title}</a> <${feature_url ? 'a href="' + feature_url + '"' : 'span'}>${title || '<i>indisponible</i>'}</${feature_url ? 'a' : 'span'}>
</h4> </h4>
<div> <div>
Statut : ${status} Statut : ${status || '<i>indisponible</i>'}
</div> </div>
<div> <div>
Type : <a href="${feature_type_url}"> ${feature_type.title} </a> Type : <${feature_type_url ? 'a href="'+ feature_type_url + '"' : 'span'}> ${feature_type.title || '<i>indisponible</i>'} </${feature_type_url ? 'a' : 'span'}>
</div> </div>
<div> <div>
Dernière mise à jour : ${date_maj} Dernière mise à jour : ${date_maj || '<i>indisponible</i>'}
</div> </div>
${author} ${author}
`; `;
}, },
}; };
......
...@@ -7,7 +7,28 @@ export function fileConvertSize(aSize){ ...@@ -7,7 +7,28 @@ export function fileConvertSize(aSize){
} }
export function fileConvertSizeToMo(aSize){ export function fileConvertSizeToMo(aSize){
console.log(aSize);
aSize = Math.abs(parseInt(aSize, 10)); aSize = Math.abs(parseInt(aSize, 10));
const def = [1024*1024, 'Mo', 1]; const def = [1024*1024, 'Mo', 1];
return (aSize/def[0]).toFixed(def[2]); return (aSize/def[0]).toFixed(def[2]);
} }
export function csvToJson(csv) {
let result = [];
const allLines = csv.split('\n');
const headers = allLines[0].split(',');
const [, ...lines] = allLines;
for (const line of lines) {
let obj = {};
const currentLine = line.split(',');
for (let i = 0; i < headers.length; i++) {
obj[headers[i]] = currentLine[i];
}
result.push(obj);
}
return JSON.parse(JSON.stringify(result));
}
...@@ -125,11 +125,10 @@ export default { ...@@ -125,11 +125,10 @@ export default {
methods: { methods: {
toggleDropdown(val) { toggleDropdown(val) {
if (this.isOpen) { if (this.isOpen) { //* if dropdown is open :
this.input = ''; // * clear input field when closing dropdown this.input = ''; // * -> clear input field when closing dropdown
} else if (this.search) { } else if (this.search) { //* if dropdown is closed is a search dropdown:
//* focus on input if is a search dropdown this.$refs.input.focus({ //* -> focus on input field
this.$refs.input.focus({
preventScroll: true, preventScroll: true,
}); });
} else if (this.clearable && val.target && this.selected) { } else if (this.clearable && val.target && this.selected) {
...@@ -172,8 +171,9 @@ export default { ...@@ -172,8 +171,9 @@ export default {
}, },
clickOutsideDropdown(e) { clickOutsideDropdown(e) {
if (!e.target.closest(`#custom-dropdown${this.identifier}`)) if (!e.target.closest(`#custom-dropdown${this.identifier}`) && this.isOpen) {
this.toggleDropdown(false); this.toggleDropdown(false);
}
}, },
}, },
}; };
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
selected-label="" selected-label=""
deselect-label="" deselect-label=""
:searchable="true" :searchable="true"
:placeholder="'Rechercher un signalement ...'" :placeholder="placeholder"
:show-no-results="true" :show-no-results="true"
:loading="loading" :loading="loading"
:clear-on-select="false" :clear-on-select="false"
...@@ -52,12 +52,20 @@ export default { ...@@ -52,12 +52,20 @@ export default {
Multiselect Multiselect
}, },
props: {
currentSelection : {
type: Object,
default: null,
}
},
data() { data() {
return { return {
loading: false, loading: false,
selection: null, selection: null,
text: null, text: null,
results: [] results: [],
placeholder: 'Rechercher un signalement ...'
}; };
}, },
...@@ -91,6 +99,12 @@ export default { ...@@ -91,6 +99,12 @@ export default {
this.RESET_CANCELLABLE_SEARCH_REQUEST(); this.RESET_CANCELLABLE_SEARCH_REQUEST();
}, },
mounted() {
if (this.currentSelection && this.currentSelection.feature_to) {
this.placeholder = this.currentSelection.feature_to.title;
}
},
methods: { methods: {
...mapMutations(['RESET_CANCELLABLE_SEARCH_REQUEST']), ...mapMutations(['RESET_CANCELLABLE_SEARCH_REQUEST']),
...mapActions('feature', [ ...mapActions('feature', [
...@@ -109,6 +123,19 @@ export default { ...@@ -109,6 +123,19 @@ export default {
}; };
</script> </script>
<style scoped> <style>
.multiselect input {
line-height: 1em !important;
padding: 0 !important;
}
.multiselect__placeholder {
margin: 0;
padding: 0;
}
.multiselect__input {
min-height: auto !important;
line-height: 1em !important;
}
</style> </style>
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
<div class="visible-fields"> <div class="visible-fields">
<div class="two fields"> <div class="two fields">
<div class="required field"> <div class="required field">
<label for="form.relation_type.id_for_label">{{ <label :for="form.relation_type.id_for_label">{{
form.relation_type.label form.relation_type.label
}}</label> }}</label>
<Dropdown <Dropdown
...@@ -35,10 +35,11 @@ ...@@ -35,10 +35,11 @@
{{ form.relation_type.errors }} {{ form.relation_type.errors }}
</div> </div>
<div class="required field"> <div class="required field">
<label for="form.feature_to.id_for_label">{{ <label :for="form.feature_to.id_for_label">{{
form.feature_to.label form.feature_to.label
}}</label> }}</label>
<SearchFeature <SearchFeature
:current-selection="linkedForm"
@select="selectFeatureTo" @select="selectFeatureTo"
@close="selectFeatureTo" @close="selectFeatureTo"
/> />
...@@ -90,10 +91,7 @@ export default { ...@@ -90,10 +91,7 @@ export default {
}, },
html_name: 'feature_to', html_name: 'feature_to',
label: 'Signalement lié', label: 'Signalement lié',
value: { value: '',
name: '',
value: '',
},
}, },
}, },
relationTypeChoices: [ relationTypeChoices: [
...@@ -106,7 +104,6 @@ export default { ...@@ -106,7 +104,6 @@ export default {
}, },
computed: { computed: {
selected_relation_type: { selected_relation_type: {
// getter // getter
get() { get() {
...@@ -122,23 +119,24 @@ export default { ...@@ -122,23 +119,24 @@ export default {
mounted() { mounted() {
if (this.linkedForm.relation_type) { if (this.linkedForm.relation_type) {
this.getExistingRelation_type(); this.form.relation_type.value.name = this.linkedForm.relation_type_display;
this.form.relation_type.value.value = this.linkedForm.relation_type;
}
if (this.linkedForm.feature_to) {
this.form.feature_to.value = this.linkedForm.feature_to.feature_id;
} }
}, },
methods: { methods: {
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
},
remove_linked_formset() { remove_linked_formset() {
this.$store.commit('feature/REMOVE_LINKED_FORM', this.linkedForm.dataKey); this.$store.commit('feature/REMOVE_LINKED_FORM', this.linkedForm.dataKey);
}, },
selectFeatureTo(e) { selectFeatureTo(feature) {
this.form.feature_to.value = e; if (feature && feature.feature_id) {
this.form.feature_to.value = feature.feature_id;
this.updateStore();
}
}, },
updateStore() { updateStore() {
...@@ -146,7 +144,7 @@ export default { ...@@ -146,7 +144,7 @@ export default {
dataKey: this.linkedForm.dataKey, dataKey: this.linkedForm.dataKey,
relation_type: this.form.relation_type.value.value, relation_type: this.form.relation_type.value.value,
feature_to: { feature_to: {
feature_id: this.form.feature_to.value.value, feature_id: this.form.feature_to.value,
}, },
}); });
}, },
...@@ -154,7 +152,7 @@ export default { ...@@ -154,7 +152,7 @@ export default {
checkForm() { checkForm() {
if (this.form.feature_to.value === '') { if (this.form.feature_to.value === '') {
this.form.errors = [ this.form.errors = [
'<strong>Choisir un signalement lié</strong><br/> Pourriez-vous choisir un signalement pour la nouvelle liaison ?', 'Veuillez choisir un signalement pour la nouvelle liaison.',
]; ];
document document
.getElementById('errorlist-links') .getElementById('errorlist-links')
...@@ -164,24 +162,6 @@ export default { ...@@ -164,24 +162,6 @@ export default {
this.form.errors = []; this.form.errors = [];
return true; return true;
}, },
getExistingFeature_to(featureOptions) {
const feature_to = featureOptions.find(
(el) => el.value === this.linkedForm.feature_to.feature_id
);
if (feature_to) {
this.form.feature_to.value = feature_to;
}
},
getExistingRelation_type() {
const relation_type = this.relationTypeChoices.find(
(el) => el.value === this.linkedForm.relation_type
);
if (relation_type) {
this.form.relation_type.value = relation_type;
}
},
}, },
}; };
</script> </script>
\ No newline at end of file
...@@ -125,10 +125,10 @@ ...@@ -125,10 +125,10 @@
:class="['ui checkbox', {disabled: !checkRights(feature)}]" :class="['ui checkbox', {disabled: !checkRights(feature)}]"
> >
<input <input
:id="feature.id" :id="feature.feature_id"
v-model="checked" v-model="checked"
type="checkbox" type="checkbox"
:value="feature.id" :value="feature.feature_id"
:disabled="!checkRights(feature)" :disabled="!checkRights(feature)"
name="select" name="select"
@input="storeClickedFeature(feature)" @input="storeClickedFeature(feature)"
...@@ -138,25 +138,29 @@ ...@@ -138,25 +138,29 @@
</td> </td>
<td class="dt-center"> <td class="dt-center">
<div v-if="feature.properties.status.value === 'archived'"> <div
<span data-tooltip="Archivé"> v-if="feature.status === 'archived'"
<i class="grey archive icon" /> data-tooltip="Archivé"
</span> >
<i class="grey archive icon" />
</div> </div>
<div v-else-if="feature.properties.status.value === 'pending'"> <div
<span data-tooltip="En attente de publication"> v-else-if="feature.status === 'pending'"
<i class="teal hourglass outline icon" /> data-tooltip="En attente de publication"
</span> >
<i class="teal hourglass outline icon" />
</div> </div>
<div v-else-if="feature.properties.status.value === 'published'"> <div
<span data-tooltip="Publié"> v-else-if="feature.status === 'published'"
<i class="olive check icon" /> data-tooltip="Publié"
</span> >
<i class="olive check icon" />
</div> </div>
<div v-else-if="feature.properties.status.value === 'draft'"> <div
<span data-tooltip="Brouillon"> v-else-if="feature.status === 'draft'"
<i class="orange pencil alternate icon" /> data-tooltip="Brouillon"
</span> >
<i class="orange pencil alternate icon" />
</div> </div>
</td> </td>
<td class="dt-center"> <td class="dt-center">
...@@ -164,11 +168,11 @@ ...@@ -164,11 +168,11 @@
:to="{ :to="{
name: 'details-type-signalement', name: 'details-type-signalement',
params: { params: {
feature_type_slug: feature.properties.feature_type.slug, feature_type_slug: feature.feature_type.slug,
}, },
}" }"
> >
{{ feature.properties.feature_type.title }} {{ feature.feature_type.title }}
</router-link> </router-link>
</td> </td>
<td class="dt-center"> <td class="dt-center">
...@@ -176,28 +180,28 @@ ...@@ -176,28 +180,28 @@
:to="{ :to="{
name: 'details-signalement', name: 'details-signalement',
params: { params: {
slug_type_signal: feature.properties.feature_type.slug, slug_type_signal: feature.feature_type.slug,
slug_signal: feature.properties.slug || feature.id, slug_signal: feature.slug || feature.feature_id,
}, },
}" }"
> >
{{ getFeatureDisplayName(feature) }} {{ feature.title || feature.feature_id }}
</router-link> </router-link>
</td> </td>
<td class="dt-center"> <td class="dt-center">
{{ feature.properties.updated_on }} {{ feature.updated_on | formatDate }}
</td> </td>
<td <td
v-if="user" v-if="user"
class="dt-center" class="dt-center"
> >
{{ getUserName(feature) }} {{ feature.display_creator || ' ---- ' }}
</td> </td>
<td <td
v-if="user" v-if="user"
class="dt-center" class="dt-center"
> >
{{ feature.properties.display_last_editor }} {{ feature.display_last_editor || ' ---- ' }}
</td> </td>
</tr> </tr>
<tr <tr
...@@ -296,11 +300,17 @@ ...@@ -296,11 +300,17 @@
<script> <script>
import { mapState, mapGetters, mapMutations } from 'vuex'; import { mapState, mapGetters, mapMutations } from 'vuex';
import FeatureListMassToggle from '@/components/feature/FeatureListMassToggle'; import FeatureListMassToggle from '@/components/feature/FeatureListMassToggle';
import { formatStringDate } from '@/utils';
export default { export default {
name: 'FeatureListTable', name: 'FeatureListTable',
filters: {
formatDate(value) {
return formatStringDate(value);
},
},
components: { components: {
FeatureListMassToggle, FeatureListMassToggle,
}, },
...@@ -400,7 +410,7 @@ export default { ...@@ -400,7 +410,7 @@ export default {
canDeleteFeature(feature) { canDeleteFeature(feature) {
if (this.userStatus === 'Administrateur projet') return true; //* can delete all if (this.userStatus === 'Administrateur projet') return true; //* can delete all
//* others can delete only their own features //* others can delete only their own features
return feature.properties.creator.username === this.user.username; return feature.display_creator === this.user.username;
}, },
canEditFeature(feature) { canEditFeature(feature) {
...@@ -411,10 +421,10 @@ export default { ...@@ -411,10 +421,10 @@ export default {
Contributeur : ['draft', 'pending', 'published'], Contributeur : ['draft', 'pending', 'published'],
}; };
if (this.userStatus === 'Contributeur' && feature.properties.creator.username !== this.user.username) { if (this.userStatus === 'Contributeur' && feature.display_creator !== this.user.username) {
return false; return false;
} else if (permissions[this.userStatus]) { } else if (permissions[this.userStatus]) {
return permissions[this.userStatus].includes(feature.properties.status.value); return permissions[this.userStatus].includes(feature.status);
} else { } else {
return false; return false;
} }
...@@ -429,14 +439,10 @@ export default { ...@@ -429,14 +439,10 @@ export default {
} }
}, },
getUserName(feature) { switchMode() {
if (!feature.properties.creator) { this.$emit('update:mode', this.mode === 'modify' ? 'delete' : 'modify');
return ' ---- '; this.UPDATE_CLICKED_FEATURES([]);
} this.$store.commit('feature/UPDATE_CHECKED_FEATURES', []);
return feature.properties.creator.username || ' ---- ';
},
getFeatureDisplayName(feature) {
return feature.properties.title || feature.id;
}, },
isSortedAsc(column) { isSortedAsc(column) {
......
...@@ -52,9 +52,9 @@ if (workbox) { ...@@ -52,9 +52,9 @@ if (workbox) {
}) })
); );
workbox.routing.registerRoute( workbox.routing.registerRoute(
/^https:\/\/osm\.geo2france\.fr\/mapcache/, new RegExp('.*/service=WMS&request=GetMap/.*'),
new workbox.strategies.CacheFirst({ new workbox.strategies.CacheFirst({
cacheName: 'mapcache', cacheName: 'wms',
plugins: [ plugins: [
new workbox.cacheableResponse.Plugin({ new workbox.cacheableResponse.Plugin({
statuses: [0, 200], statuses: [0, 200],
......
...@@ -13,8 +13,8 @@ const feature = { ...@@ -13,8 +13,8 @@ const feature = {
features_count: 0, features_count: 0,
current_feature: [], current_feature: [],
form: null, form: null,
linkedFormset: [], linkedFormset: [], //* used to edit in feature_edit
linked_features: [], linked_features: [], //* used to display in feature_detail
massMode: 'modify', massMode: 'modify',
statusChoices: [ statusChoices: [
{ {
...@@ -168,7 +168,7 @@ const feature = { ...@@ -168,7 +168,7 @@ const feature = {
const cancelToken = axios.CancelToken.source(); const cancelToken = axios.CancelToken.source();
commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true }); commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true });
commit('SET_CURRENT_FEATURE', null); //commit('SET_CURRENT_FEATURE', null); //? Est-ce que c'est nécessaire ? -> fait sauter l'affichage au clic sur un signalement lié (feature_detail)
let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}projects/${project_slug}/feature/?id=${feature_id}`; let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}projects/${project_slug}/feature/?id=${feature_id}`;
return axios return axios
.get(url, { cancelToken: cancelToken.token }) .get(url, { cancelToken: cancelToken.token })
......
...@@ -210,6 +210,35 @@ const feature_type = { ...@@ -210,6 +210,35 @@ const feature_type = {
}); });
}, },
async SEND_FEATURES_FROM_CSV({ state, dispatch }, payload) {
let { feature_type_slug, csv } = payload;
if(!csv && !state.fileToImport && state.fileToImport.size === 0 ) return;
let formData = new FormData();
formData.append('csv_file', state.fileToImport);
formData.append('feature_type_slug', feature_type_slug);
const url = `${this.state.configuration.VUE_APP_DJANGO_API_BASE}import-tasks/`;
return axios
.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then((response) => {
if (response && response.status === 200) {
return dispatch('GET_IMPORTS', {
feature_type: feature_type_slug
});
}
return response;
})
.catch((error) => {
throw (error);
});
},
GET_IMPORTS({ commit }, { project_slug, feature_type }) { GET_IMPORTS({ commit }, { project_slug, feature_type }) {
let url = `${this.state.configuration.VUE_APP_DJANGO_API_BASE}import-tasks/`; let url = `${this.state.configuration.VUE_APP_DJANGO_API_BASE}import-tasks/`;
if (project_slug) { if (project_slug) {
......
export function parseDate(date) { export function formatStringDate(stringDate) {
let dateArr = date.split('/').reverse(); const date = new Date(stringDate);
return new Date(dateArr[0], dateArr[1] - 1, dateArr[2]); const formatted_date = date.getFullYear() + '/' + ('0' + (date.getMonth() + 1)).slice(-2) + '/' + ('0' + date.getDate()).slice(-2) + ' ' +
('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2);
return formatted_date;
} }
export function allowedStatus2change(statusChoices, isModerate, userStatus, isOwnFeature, currentRouteName) { export function allowedStatus2change(statusChoices, isModerate, userStatus, isOwnFeature, currentRouteName) {
......
This diff is collapsed.
...@@ -647,8 +647,7 @@ export default { ...@@ -647,8 +647,7 @@ export default {
for (const linked of linkedFormset) { for (const linked of linkedFormset) {
this.$store.commit('feature/ADD_LINKED_FORM', { this.$store.commit('feature/ADD_LINKED_FORM', {
dataKey: this.linkedDataKey, dataKey: this.linkedDataKey,
relation_type: linked.relation_type, ...linked
feature_to: linked.feature_to,
}); });
this.linkedDataKey += 1; this.linkedDataKey += 1;
} }
...@@ -984,6 +983,10 @@ export default { ...@@ -984,6 +983,10 @@ export default {
const allFeaturesExceptCurrent = features.filter( const allFeaturesExceptCurrent = features.filter(
(feat) => feat.id !== currentFeatureId (feat) => feat.id !== currentFeatureId
); );
console.log(allFeaturesExceptCurrent);
console.log(allFeaturesExceptCurrent[0].geometry);
console.log(allFeaturesExceptCurrent[0].geometry.coordinates);
console.log(allFeaturesExceptCurrent[0].geometry.coordinates[0]);
mapUtil.addFeatures( mapUtil.addFeatures(
allFeaturesExceptCurrent, allFeaturesExceptCurrent,
{}, {},
......
...@@ -590,7 +590,7 @@ export default { ...@@ -590,7 +590,7 @@ export default {
}, },
fetchPagedFeatures(newUrl) { fetchPagedFeatures(newUrl) {
let url = `${this.API_BASE_URL}projects/${this.projectSlug}/feature-paginated/?output=geojson&limit=${this.pagination.pagesize}&offset=${this.pagination.start}`; let url = `${this.API_BASE_URL}projects/${this.projectSlug}/feature-paginated/?limit=${this.pagination.pagesize}&offset=${this.pagination.start}`;
//* if receiving next & previous url (// todo : might be not used anymore, to check) //* if receiving next & previous url (// todo : might be not used anymore, to check)
if (newUrl && typeof newUrl === 'string') { if (newUrl && typeof newUrl === 'string') {
//newUrl = newUrl.replace("8000", "8010"); //* for dev uncomment to use proxy link //newUrl = newUrl.replace("8000", "8010"); //* for dev uncomment to use proxy link
...@@ -608,7 +608,7 @@ export default { ...@@ -608,7 +608,7 @@ export default {
this.featuresCount = data.count; this.featuresCount = data.count;
this.previous = data.previous; this.previous = data.previous;
this.next = data.next; this.next = data.next;
this.paginatedFeatures = data.results.features; this.paginatedFeatures = data.results;
} }
//* bbox needs to be updated with the same filters //* bbox needs to be updated with the same filters
if (this.paginatedFeatures.length) { if (this.paginatedFeatures.length) {
......
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
for="json_file" for="json_file"
> >
<i class="file icon" /> <i class="file icon" />
<span class="label">{{ fileToImport.name }}</span> <span class="label">{{ geojsonFileToImport.name }}</span>
</label> </label>
<input <input
id="json_file" id="json_file"
...@@ -97,7 +97,25 @@ ...@@ -97,7 +97,25 @@
accept="application/json, .json, .geojson" accept="application/json, .json, .geojson"
style="display: none" style="display: none"
name="json_file" name="json_file"
@change="onFileChange" @change="onGeojsonFileChange"
>
</div>
<div class="field">
<label
class="ui icon button ellipsis"
for="csv_file"
>
<i class="file icon" />
<span class="label">{{ csvFileToImport.name }}</span>
</label>
<input
id="csv_file"
type="file"
accept="application/csv, .csv"
style="display: none"
name="csv_file"
@change="onCsvFileChange"
> >
</div> </div>
...@@ -133,9 +151,12 @@ ...@@ -133,9 +151,12 @@
</li> </li>
</ul> </ul>
<button <button
:disabled="fileToImport.size === 0 && !$route.params.geojson" :disabled="
(geojsonFileToImport.size === 0 && !$route.params.geojson) &&
(csvFileToImport.size === 0 && !$route.params.csv)
"
class="ui fluid teal icon button" class="ui fluid teal icon button"
@click="importGeoJson" @click="geojsonFileToImport.size !== 0 ? importGeoJson() : importCSV()"
> >
<i class="upload icon" /> Lancer l'import <i class="upload icon" /> Lancer l'import
</button> </button>
...@@ -160,6 +181,30 @@ ...@@ -160,6 +181,30 @@
Vous pouvez télécharger tous les signalements qui vous sont Vous pouvez télécharger tous les signalements qui vous sont
accessibles. accessibles.
</p> </p>
<!-- <div class="ui selection dropdown fluid">
<input type="hidden" name="format">
<i class="dropdown icon"></i>
<div class="default text">Format</div>
<div class="menu">
<div class="item" data-value="1">GeoJSON</div>
<div class="item" data-value="2">CSV</div>
</div>
</div> -->
<select
v-model="exportFormat"
class="ui fluid dropdown"
style="margin-bottom: 1em;"
>
<option value="GeoJSON">
GeoJSON
</option>
<option value="CSV">
CSV
</option>
</select>
<button <button
type="button" type="button"
class="ui fluid teal icon button" class="ui fluid teal icon button"
...@@ -287,10 +332,11 @@ ...@@ -287,10 +332,11 @@
<script> <script>
import { mapActions, mapMutations, mapGetters, mapState } from 'vuex'; import { mapActions, mapMutations, mapGetters, mapState } from 'vuex';
import { formatStringDate } from '@/utils';
import ImportTask from '@/components/ImportTask'; import ImportTask from '@/components/ImportTask';
import featureAPI from '@/services/feature-api'; import featureAPI from '@/services/feature-api';
import { fileConvertSizeToMo } from '@/assets/js/utils'; import { fileConvertSizeToMo, csvToJson } from '@/assets/js/utils';
export default { export default {
name: 'FeatureTypeDetail', name: 'FeatureTypeDetail',
...@@ -301,9 +347,7 @@ export default { ...@@ -301,9 +347,7 @@ export default {
filters: { filters: {
formatDate(value) { formatDate(value) {
let date = new Date(value); return formatStringDate(value);
date = date.toLocaleString().replace(',', ' à');
return date.substr(0, date.length - 3); //* quick & dirty way to remove seconds from date
}, },
}, },
...@@ -312,21 +356,30 @@ export default { ...@@ -312,21 +356,30 @@ export default {
type: Object, type: Object,
default: null, default: null,
}, },
csv: {
type: Object,
default: null,
},
}, },
data() { data() {
return { return {
importError: '', importError: '',
fileToImport: { geojsonFileToImport: {
name: 'Sélectionner un fichier GeoJSON ...', name: 'Sélectionner un fichier GeoJSON ...',
size: 0, size: 0,
}, },
csvFileToImport: {
name: 'Sélectionner un fichier CSV ...',
size: 0,
},
showImport: false, showImport: false,
slug: this.$route.params.slug, slug: this.$route.params.slug,
featuresLoading: true, featuresLoading: true,
loadingImportFile: false, loadingImportFile: false,
waitMessage: false, waitMessage: false,
reloadingImport: false, reloadingImport: false,
exportFormat: 'GeoJSON'
}; };
}, },
...@@ -504,7 +557,64 @@ export default { ...@@ -504,7 +557,64 @@ export default {
return true; return true;
}, },
onFileChange(e) { checkCsvValidity(csv) {
this.importError = '';
// Check if file contains 'lat' and 'long' fields
const headersLine =
csv
.split('\n')[0]
.split(',')
.filter(el => {
return el === 'lat' || el === 'lon';
});
// Look for 2 decimal fields in first line of csv
// corresponding to lon and lat
const sampleLine =
csv
.split('\n')[1]
.split(',')
.map(el => {
return !isNaN(el) && el.indexOf('.') != -1;
})
.filter(Boolean);
if (sampleLine.length > 1 && headersLine.length === 2) {
const fields = this.structure.customfield_set.map((el) => {
return {
name: el.name,
field_type: el.field_type,
options: el.options,
};
});
const csvFeatures = csvToJson(csv);
for (const feature of csvFeatures) {
for (const { name, field_type, options } of fields) {
if (name in feature) {
const fieldInFeature = feature[name];
const customType = this.transformProperties(fieldInFeature);
//* if custom field value is not null, then check validity of field
if (fieldInFeature !== null) {
//* if field type is list, it's not possible to guess from value type
if (field_type === 'list') {
//*then check if the value is an available option
if (!options.includes(fieldInFeature)) {
return false;
}
} else if (customType !== field_type) {
//* check if custom field value match
this.importError = `Le fichier est invalide: Un champ de type ${field_type} ne peut pas avoir la valeur [ ${fieldInFeature} ]`;
return false;
}
}
}
}
}
return true;
} else {
return false;
}
},
onGeojsonFileChange(e) {
this.loadingImportFile = true; this.loadingImportFile = true;
const files = e.target.files || e.dataTransfer.files; const files = e.target.files || e.dataTransfer.files;
if (!files.length) { if (!files.length) {
...@@ -523,10 +633,42 @@ export default { ...@@ -523,10 +633,42 @@ export default {
} }
if (jsonValidity) { if (jsonValidity) {
this.fileToImport = files[0]; // todo : remove this value from state as it stored (first attempt didn't work) this.geojsonFileToImport = files[0]; // todo : remove this value from state as it stored (first attempt didn't work)
this.$store.commit( this.$store.commit(
'feature_type/SET_FILE_TO_IMPORT', 'feature_type/SET_FILE_TO_IMPORT',
this.fileToImport this.geojsonFileToImport
);
}
this.loadingImportFile = false;
});
reader.readAsText(files[0]);
},
onCsvFileChange(e) {
this.loadingImportFile = true;
const files = e.target.files || e.dataTransfer.files;
console.log(files);
if (!files.length) {
this.loadingImportFile = false;
return;
}
let reader = new FileReader();
reader.addEventListener('load', (e) => {
console.log(e);
// bypass csv check for files larger then 10 Mo
let csvValidity;
if (parseFloat(fileConvertSizeToMo(files[0].size)) <= 10) {
csvValidity = this.checkCsvValidity(e.target.result);
} else {
csvValidity = true;
}
if (csvValidity) {
this.csvFileToImport = files[0]; // todo : remove this value from state as it stored (first attempt didn't work)
this.$store.commit(
'feature_type/SET_FILE_TO_IMPORT',
this.csvFileToImport
); );
} }
this.loadingImportFile = false; this.loadingImportFile = false;
...@@ -542,8 +684,8 @@ export default { ...@@ -542,8 +684,8 @@ export default {
}; };
if (this.$route.params.geojson) { //* import after redirection, for instance with data from catalog if (this.$route.params.geojson) { //* import after redirection, for instance with data from catalog
payload['geojson'] = this.$route.params.geojson; payload['geojson'] = this.$route.params.geojson;
} else if (this.fileToImport.size > 0) { //* import directly from geojson } else if (this.geojsonFileToImport.size > 0) { //* import directly from geojson
payload['fileToImport'] = this.fileToImport; payload['fileToImport'] = this.geojsonFileToImport;
} else { } else {
this.importError = "La ressource n'a pas pu être récupéré."; this.importError = "La ressource n'a pas pu être récupéré.";
return; return;
...@@ -554,6 +696,26 @@ export default { ...@@ -554,6 +696,26 @@ export default {
}); });
}, },
importCSV() {
this.waitMessage = true;
let payload = {
slug: this.slug,
feature_type_slug: this.$route.params.feature_type_slug,
};
if (this.$route.params.csv) { //* import after redirection, for instance with data from catalog
payload['csv'] = this.$route.params.csv;
} else if (this.csvFileToImport.size > 0) { //* import directly from geojson
payload['fileToImport'] = this.csvFileToImport;
} else {
this.importError = "La ressource n'a pas pu être récupéré.";
return;
}
this.$store.dispatch('feature_type/SEND_FEATURES_FROM_CSV', payload)
.then(() => {
this.waitMessage = false;
});
},
exportFeatures() { exportFeatures() {
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.slug}/feature-type/${this.$route.params.feature_type_slug}/export/`; const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.slug}/feature-type/${this.$route.params.feature_type_slug}/export/`;
featureAPI.getFeaturesBlob(url).then((blob) => { featureAPI.getFeaturesBlob(url).then((blob) => {
......
...@@ -64,7 +64,10 @@ ...@@ -64,7 +64,10 @@
</ul> </ul>
</div> </div>
<div class="required field"> <div
:class="{ disabled: csv }"
class="required field"
>
<label :for="form.geom_type.id_for_label">{{ <label :for="form.geom_type.id_for_label">{{
form.geom_type.label form.geom_type.label
}}</label> }}</label>
...@@ -147,46 +150,113 @@ ...@@ -147,46 +150,113 @@
<span v-if="action === 'duplicate' || action === 'edit'" /> <span v-if="action === 'duplicate' || action === 'edit'" />
<div id="formsets"> <!-- <div
<FeatureTypeCustomForm v-if="csvFields && csvFields.length"
v-for="customForm in customForms" id="csv-fields"
:key="customForm.dataKey"
ref="customForms"
:data-key="customForm.dataKey"
:custom-form="customForm"
:selected-color-style="form.colors_style.value.custom_field_name"
@update="updateColorsStyle($event)"
/>
</div>
<button
id="add-field"
type="button"
class="ui compact basic button"
@click="addCustomForm"
>
<i class="ui plus icon" />Ajouter un champ personnalisé
</button>
<div class="ui divider" />
<button
class="ui teal icon button margin-25"
type="button"
@click="sendFeatureType"
>
<i class="white save icon" />
{{ action === "create" ? "Créer" : "Sauvegarder" }} le type de
signalement
</button>
<button
v-if="geojson"
class="ui teal icon button margin-25"
type="button"
@click="postFeatureTypeThenFeatures"
> >
<i class="white save icon" /> <table class="ui striped table">
Créer et importer le(s) signalement(s) du geojson <thead>
</button> <tr>
<th>Champ</th>
<th>X</th>
<th>Y</th>
</tr>
</thead>
<tbody>
<tr
v-for="field in csvFields"
:key="field.field"
>
<td>
{{ field.field }}
</td>
<td>
<div
class="ui radio checkbox"
:class="{ disabled: field.y }"
>
<input
:disabled="field.y"
type="radio"
name="x"
@input="pickXcsvCoordField(field)"
>
<label />
</div>
</td>
<td>
<div
class="ui radio checkbox"
:class="{ disabled: field.x }"
>
<input
:disabled="field.x"
type="radio"
name="y"
@input="pickYcsvCoordField(field)"
>
<label />
</div>
</td>
</tr>
</tbody>
</table>
<button
class="ui teal icon button margin-25"
type="button"
:disabled="
!csvFields.some(el => el.x === true) ||
!csvFields.some(el => el.y === true)
"
@click="setCSVCoordsFields"
>
<i class="white save icon" />
Continuer
</button>
</div> -->
<div v-else>
<div id="formsets">
<FeatureTypeCustomForm
v-for="customForm in customForms"
:key="customForm.dataKey"
ref="customForms"
:data-key="customForm.dataKey"
:custom-form="customForm"
:selected-color-style="form.colors_style.value.custom_field_name"
@update="updateColorsStyle($event)"
/>
</div>
<button
id="add-field"
type="button"
class="ui compact basic button"
@click="addCustomForm"
>
<i class="ui plus icon" />Ajouter un champ personnalisé
</button>
<div class="ui divider" />
<button
class="ui teal icon button margin-25"
type="button"
@click="sendFeatureType"
>
<i class="white save icon" />
{{ action === "create" ? "Créer" : "Sauvegarder" }} le type de
signalement
</button>
<button
v-if="geojson || csv"
class="ui teal icon button margin-25"
type="button"
@click="postFeatureTypeThenFeatures"
>
<i class="white save icon" />
Créer et importer le(s) signalement(s) du geojson
</button>
</div>
</form> </form>
</div> </div>
</div> </div>
...@@ -215,6 +285,10 @@ export default { ...@@ -215,6 +285,10 @@ export default {
type: Object, type: Object,
default: null, default: null,
}, },
csv: {
type: Array,
default: null,
},
}, },
data() { data() {
...@@ -223,6 +297,7 @@ export default { ...@@ -223,6 +297,7 @@ export default {
action: 'create', action: 'create',
dataKey: 0, dataKey: 0,
error: null, error: null,
csvFields: null,
geomTypeChoices: [ geomTypeChoices: [
{ value: 'linestring', name: 'Ligne' }, { value: 'linestring', name: 'Ligne' },
{ value: 'point', name: 'Point' }, { value: 'point', name: 'Point' },
...@@ -287,6 +362,8 @@ export default { ...@@ -287,6 +362,8 @@ export default {
'display_last_editor', 'display_last_editor',
'project', 'project',
'creator', 'creator',
'lat',
'lon'
], ],
}; };
}, },
...@@ -399,11 +476,20 @@ export default { ...@@ -399,11 +476,20 @@ export default {
this.importGeoJsonFeatureType(); this.importGeoJsonFeatureType();
if (this.fileToImport && this.fileToImport.name) { if (this.fileToImport && this.fileToImport.name) {
this.form.title.value = // * use the filename as title by default this.form.title.value = // * use the filename as title by default
this.fileToImport.name.split('.')[0]; this.fileToImport.name.split('.')[0];
} else { //* case when the geojson comes from datasud catalog } else { //* case when the geojson comes from datasud catalog
this.form.title.value = this.geojson.name;// * use the typename as title by default this.form.title.value = this.geojson.name;// * use the typename as title by default
} }
} }
if (this.csv) {
this.importCSVFeatureType();
if (this.fileToImport && this.fileToImport.name) {
this.form.title.value = // * use the filename as title by default
this.fileToImport.name.split('.')[0];
} else { //* case when the geojson comes from datasud catalog
this.form.title.value = this.csv.name;// * use the typename as title by default
}
}
}, },
beforeDestroy() { beforeDestroy() {
this.$store.commit('feature_type/EMPTY_FORM'); this.$store.commit('feature_type/EMPTY_FORM');
...@@ -553,7 +639,7 @@ export default { ...@@ -553,7 +639,7 @@ export default {
} }
}, },
postFeatures(feature_type_slug) { postGeojsonFeatures(feature_type_slug) {
this.$store this.$store
.dispatch('feature_type/SEND_FEATURES_FROM_GEOJSON', { .dispatch('feature_type/SEND_FEATURES_FROM_GEOJSON', {
slug: this.slug, slug: this.slug,
...@@ -575,6 +661,28 @@ export default { ...@@ -575,6 +661,28 @@ export default {
this.loading = false; this.loading = false;
}); });
}, },
postCSVFeatures(feature_type_slug) {
this.$store
.dispatch('feature_type/SEND_FEATURES_FROM_CSV', {
slug: this.slug,
feature_type_slug,
csv: this.csv
})
.then((response) => {
if (response && response.status === 200) {
this.goBackToProject();
} else {
this.displayMessage(
"Une erreur est survenue lors de l'import de signalements.\n " +
response.data.detail
);
}
this.loading = false;
})
.catch(() => {
this.loading = false;
});
},
async postFeatureTypeThenFeatures() { async postFeatureTypeThenFeatures() {
const requestType = this.action === 'edit' ? 'put' : 'post'; const requestType = this.action === 'edit' ? 'put' : 'post';
...@@ -584,7 +692,12 @@ export default { ...@@ -584,7 +692,12 @@ export default {
.dispatch('feature_type/SEND_FEATURE_TYPE', requestType) .dispatch('feature_type/SEND_FEATURE_TYPE', requestType)
.then(({ feature_type_slug }) => { .then(({ feature_type_slug }) => {
if (feature_type_slug) { if (feature_type_slug) {
this.postFeatures(feature_type_slug); if (this.geojson) {
this.postGeojsonFeatures(feature_type_slug);
}
else if (this.csv) {
this.postCSVFeatures(feature_type_slug);
}
} else { } else {
this.loading = false; this.loading = false;
} }
...@@ -663,6 +776,76 @@ export default { ...@@ -663,6 +776,76 @@ export default {
} }
} }
}, },
importCSVFeatureType() {
if (this.csv.length) {
this.updateStore(); //* register title & geom_type in store
// List fileds for user to select coords fields
// this.csvFields =
// Object.keys(this.csv[0])
// .map(el => {
// return {
// field: el,
// x: false,
// y:false
// };
// });
for (const [key, val] of Object.entries(this.csv[0])) {
//* check that the property is not a keyword from the backend or map style
// todo: add map style keywords
if (!this.reservedKeywords.includes(key)) {
const customForm = {
label: { value: key || '' },
name: { value: key || '' },
position: this.dataKey, // * use dataKey already incremented by addCustomForm
field_type: { value: this.transformProperties(val) }, // * guessed from the type
options: { value: [] }, // * not available in export
};
this.addCustomForm(customForm);
}
}
}
},
// pickXcsvCoordField(e) {
// this.csvFields.forEach(el => {
// if (el.field === e.field) {
// el.x = true;
// } else {
// el.x = false;
// }
// });
// },
// pickYcsvCoordField(e) {
// this.csvFields.forEach(el => {
// if (el.field === e.field) {
// el.y = true;
// } else {
// el.y = false;
// }
// });
// },
// setCSVCoordsFields() {
// const xField = this.csvFields.find(el => el.x === true).field;
// const yField = this.csvFields.find(el => el.y === true).field;
// this.csvFields = null;
// for (const [key, val] of Object.entries(this.csv[0])) {
// //* check that the property is not a keyword from the backend or map style
// // todo: add map style keywords
// if (!this.reservedKeywords.includes(key) && key !== xField && key !== yField) {
// const customForm = {
// label: { value: key || '' },
// name: { value: key || '' },
// position: this.dataKey, // * use dataKey already incremented by addCustomForm
// field_type: { value: this.transformProperties(val) }, // * guessed from the type
// options: { value: [] }, // * not available in export
// };
// this.addCustomForm(customForm);
// }
// }
// }
}, },
}; };
</script> </script>
......
...@@ -372,6 +372,7 @@ ...@@ -372,6 +372,7 @@
<i class="ui plus icon" />Créer un nouveau type de signalement <i class="ui plus icon" />Créer un nouveau type de signalement
</router-link> </router-link>
</div> </div>
<div class="nouveau-type-signalement"> <div class="nouveau-type-signalement">
<div <div
v-if=" v-if="
...@@ -403,7 +404,43 @@ ...@@ -403,7 +404,43 @@
accept="application/json, .json, .geojson" accept="application/json, .json, .geojson"
style="display: none" style="display: none"
name="json_file" name="json_file"
@change="onFileChange" @change="onGeoJSONFileChange"
>
</div>
</div>
<div class="nouveau-type-signalement">
<div
v-if="
permissions &&
permissions.can_update_project &&
isOnline
"
class="
ui
compact
basic
button
button-align-left
"
>
<i class="ui plus icon" />
<label
class="ui"
for="csv_file"
>
<span
class="label"
>Créer un nouveau type de signalement à partir d'un
CSV</span>
</label>
<input
id="csv_file"
type="file"
accept="application/csv, .csv"
style="display: none"
name="csv_file"
@change="onCSVFileChange"
> >
</div> </div>
</div> </div>
...@@ -431,19 +468,44 @@ ...@@ -431,19 +468,44 @@
</div> </div>
<div <div
v-if="fileToImport.size > 0" v-if="geojsonFileToImport.size > 0"
id="button-import"
>
<button
:disabled="geojsonFileToImport.size === 0"
class="ui fluid teal icon button"
@click="toNewGeojsonFeatureType"
>
<i class="upload icon" /> Lancer l'import avec le fichier
{{ geojsonFileToImport.name }}
</button>
</div>
<div
v-if="csvFileToImport.size > 0 && !csvError"
id="button-import" id="button-import"
> >
<button <button
:disabled="fileToImport.size === 0" :disabled="csvFileToImport.size === 0"
class="ui fluid teal icon button" class="ui fluid teal icon button"
@click="toNewFeatureType" @click="toNewCsvFeatureType"
> >
<i class="upload icon" /> Lancer l'import avec le fichier <i class="upload icon" /> Lancer l'import avec le fichier
{{ fileToImport.name }} {{ csvFileToImport.name }}
</button> </button>
</div> </div>
<div
v-if="csvError"
class="ui negative message"
>
<i
class="close icon"
@click="csvError = null"
/>
{{ csvError }}
</div>
</div> </div>
<div <div
id="map-column" id="map-column"
class="seven wide column" class="seven wide column"
...@@ -590,8 +652,8 @@ ...@@ -590,8 +652,8 @@
<h3 class="ui header"> <h3 class="ui header">
Paramètres du projet Paramètres du projet
</h3> </h3>
<div class="ui five stackable cards"> <div class="ui three stackable cards">
<div class="card"> <!-- <div class="card">
<div class="center aligned content"> <div class="center aligned content">
<h4 class="ui center aligned icon header"> <h4 class="ui center aligned icon header">
<i class="disabled grey archive icon" /> <i class="disabled grey archive icon" />
...@@ -616,7 +678,7 @@ ...@@ -616,7 +678,7 @@
<div class="center aligned extra content"> <div class="center aligned extra content">
{{ project.delete_feature }} jours {{ project.delete_feature }} jours
</div> </div>
</div> </div> -->
<div class="card"> <div class="card">
<div class="content"> <div class="content">
<h4 class="ui center aligned icon header"> <h4 class="ui center aligned icon header">
...@@ -741,7 +803,7 @@ ...@@ -741,7 +803,7 @@
<div class="content"> <div class="content">
<p> <p>
Impossible de créer un type de signalement à partir d'un fichier Impossible de créer un type de signalement à partir d'un fichier
GeoJSON de plus de 10Mo (celui importé fait {{ fileSize }} Mo). GeoJSON de plus de 10Mo (celui importé fait {{ geojsonFileSize > 0 ? geojsonFileSize : csvFileSize }} Mo).
</p> </p>
</div> </div>
<div class="actions"> <div class="actions">
...@@ -758,6 +820,8 @@ ...@@ -758,6 +820,8 @@
</template> </template>
<script> <script>
import { featureCollection, point } from '@turf/helpers';
import bbox from '@turf/bbox';
import frag from 'vue-frag'; import frag from 'vue-frag';
import { mapUtil } from '@/assets/js/map-util.js'; import { mapUtil } from '@/assets/js/map-util.js';
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex'; import { mapGetters, mapState, mapActions, mapMutations } from 'vuex';
...@@ -765,7 +829,7 @@ import projectAPI from '@/services/project-api'; ...@@ -765,7 +829,7 @@ import projectAPI from '@/services/project-api';
import featureTypeAPI from '@/services/featureType-api'; import featureTypeAPI from '@/services/featureType-api';
import featureAPI from '@/services/feature-api'; import featureAPI from '@/services/feature-api';
import { fileConvertSizeToMo } from '@/assets/js/utils'; import { fileConvertSizeToMo, csvToJson } from '@/assets/js/utils';
export default { export default {
name: 'ProjectDetails', name: 'ProjectDetails',
...@@ -774,18 +838,6 @@ export default { ...@@ -774,18 +838,6 @@ export default {
frag, frag,
}, },
filters: {
setDate(value) {
const date = new Date(value);
const d = date.toLocaleDateString('fr', {
year: '2-digit',
month: 'numeric',
day: 'numeric',
});
return d;
},
},
props: { props: {
message: { message: {
type: String, type: String,
...@@ -801,7 +853,10 @@ export default { ...@@ -801,7 +853,10 @@ export default {
arraysOfflineErrors: [], arraysOfflineErrors: [],
confirmMsg: false, confirmMsg: false,
geojsonImport: [], geojsonImport: [],
fileToImport: { name: '', size: 0 }, csvImport: null,
csvError: null,
geojsonFileToImport: { name: '', size: 0 },
csvFileToImport: { name: '', size: 0 },
slug: this.$route.params.slug, slug: this.$route.params.slug,
modalType: false, modalType: false,
is_suscriber: false, is_suscriber: false,
...@@ -854,8 +909,11 @@ export default { ...@@ -854,8 +909,11 @@ export default {
IDGO() { IDGO() {
return this.$store.state.configuration.VUE_APP_IDGO; return this.$store.state.configuration.VUE_APP_IDGO;
}, },
fileSize() { geojsonFileSize() {
return fileConvertSizeToMo(this.fileToImport.size); return fileConvertSizeToMo(this.geojsonFileToImport.size);
},
csvFileSize() {
return fileConvertSizeToMo(this.csvFileToImport.size);
}, },
isSharedProject() { isSharedProject() {
return this.$route.path.includes('projet-partage'); return this.$route.path.includes('projet-partage');
...@@ -980,7 +1038,6 @@ export default { ...@@ -980,7 +1038,6 @@ export default {
copyLink() { copyLink() {
const sharedLink = window.location.href.replace('projet', 'projet-partage'); const sharedLink = window.location.href.replace('projet', 'projet-partage');
navigator.clipboard.writeText(sharedLink).then(()=> { navigator.clipboard.writeText(sharedLink).then(()=> {
console.log('success');
this.confirmMsg = true; this.confirmMsg = true;
}, () => { }, () => {
console.log('failed'); console.log('failed');
...@@ -1061,38 +1118,50 @@ export default { ...@@ -1061,38 +1118,50 @@ export default {
localStorage.setItem('geocontrib_offline', JSON.stringify(arraysOffline)); localStorage.setItem('geocontrib_offline', JSON.stringify(arraysOffline));
}, },
toNewFeatureType() { toNewGeojsonFeatureType() {
this.featureTypeImporting = true; this.featureTypeImporting = true;
this.$router.push({ this.$router.push({
name: 'ajouter-type-signalement', name: 'ajouter-type-signalement',
params: { params: {
geojson: this.geojsonImport, geojson: this.geojsonImport,
fileToImport: this.fileToImport, fileToImport: this.geojsonFileToImport,
},
});
this.featureTypeImporting = false;
},
toNewCsvFeatureType() {
this.featureTypeImporting = true;
this.$router.push({
name: 'ajouter-type-signalement',
params: {
csv: this.csvImport,
fileToImport: this.csvFileToImport,
}, },
}); });
this.featureTypeImporting = false; this.featureTypeImporting = false;
}, },
onFileChange(e) { onGeoJSONFileChange(e) {
this.featureTypeImporting = true; this.featureTypeImporting = true;
var files = e.target.files || e.dataTransfer.files; var files = e.target.files || e.dataTransfer.files;
if (!files.length) return; if (!files.length) return;
this.fileToImport = files[0]; this.geojsonFileToImport = files[0];
// TODO : VALIDATION IF FILE IS JSON // TODO : VALIDATION IF FILE IS JSON
if (parseFloat(fileConvertSizeToMo(this.fileToImport.size)) > 10) { if (parseFloat(fileConvertSizeToMo(this.geojsonFileToImport.size)) > 10) {
this.isFileSizeModalOpen = true; this.isFileSizeModalOpen = true;
} else if (this.fileToImport.size > 0) { } else if (this.geojsonFileToImport.size > 0) {
const fr = new FileReader(); const fr = new FileReader();
try { try {
fr.onload = (e) => { fr.onload = (e) => {
this.geojsonImport = JSON.parse(e.target.result); this.geojsonImport = JSON.parse(e.target.result);
this.featureTypeImporting = false; this.featureTypeImporting = false;
}; };
fr.readAsText(this.fileToImport); fr.readAsText(this.geojsonFileToImport);
//* stock filename to import features afterward //* stock filename to import features afterward
this.$store.commit( this.$store.commit(
'feature_type/SET_FILE_TO_IMPORT', 'feature_type/SET_FILE_TO_IMPORT',
this.fileToImport this.geojsonFileToImport
); );
} catch (err) { } catch (err) {
console.error(err); console.error(err);
...@@ -1103,8 +1172,66 @@ export default { ...@@ -1103,8 +1172,66 @@ export default {
} }
}, },
onCSVFileChange(e) {
this.featureTypeImporting = true;
var files = e.target.files || e.dataTransfer.files;
if (!files.length) return;
this.csvFileToImport = files[0];
if (parseFloat(fileConvertSizeToMo(this.csvFileToImport.size)) > 10) {
this.isFileSizeModalOpen = true;
} else if (this.csvFileToImport.size > 0) {
const fr = new FileReader();
try {
fr.readAsText(this.csvFileToImport);
fr.onloadend = () => {
// Check if file contains 'lat' and 'long' fields
const headersLine =
fr.result
.split('\n')[0]
.split(',')
.filter(el => {
return el === 'lat' || el === 'lon';
});
// Look for 2 decimal fields in first line of csv
// corresponding to lon and lat
const sampleLine =
fr.result
.split('\n')[1]
.split(',')
.map(el => {
return !isNaN(el) && el.indexOf('.') != -1;
})
.filter(Boolean);
if (sampleLine.length > 1 && headersLine.length === 2) {
this.csvError = null;
this.csvImport = csvToJson(fr.result);
this.featureTypeImporting = false;
//* stock filename to import features afterward
this.$store.commit(
'feature_type/SET_FILE_TO_IMPORT',
this.csvFileToImport
);
} else {
// File doesn't seem to contain coords
this.csvError = `Le fichier ${this.csvFileToImport.name} ne semble pas contenir de coordonnées`;
this.featureTypeImporting = false;
}
};
} catch (err) {
console.error(err);
this.featureTypeImporting = false;
}
} else {
this.featureTypeImporting = false;
}
},
closeFileSizeModal() { closeFileSizeModal() {
this.fileToImport = { name: '', size: 0 }; this.geojsonFileToImport = { name: '', size: 0 };
this.csvFileToImport = { name: '', size: 0 };
this.featureTypeImporting = false; this.featureTypeImporting = false;
this.isFileSizeModalOpen = false; this.isFileSizeModalOpen = false;
}, },
...@@ -1197,32 +1324,27 @@ export default { ...@@ -1197,32 +1324,27 @@ export default {
this.$store.state.feature_type.feature_types this.$store.state.feature_type.feature_types
); );
this.mapLoading = false; this.mapLoading = false;
this.arraysOffline.forEach((x) => (x.geojson.properties.color = 'red')); const featuresOffline = this.arraysOffline.map((x) => {
const featuresOffline = this.arraysOffline.map((x) => x.geojson); return { ...x.geojson, overideColor: '#ff0000' }; //* red (hex format is better for perf)
});
this.GET_PROJECT_FEATURES({ this.featuresLoading = false;
project_slug: this.slug, mapUtil.addFeatures(
ordering: '-created_on', featuresOffline,
limit: null, {},
geojson: true, true,
}) this.$store.state.feature_type.feature_types,
.then(() => { this.$route.params.slug,
this.featuresLoading = false; );
mapUtil.addFeatures(
[...this.features, ...featuresOffline],
{},
true,
this.$store.state.feature_type.feature_types
);
})
.catch((err) => {
console.error(err);
this.featuresLoading = false;
});
featureAPI.getFeaturesBbox(this.slug).then((bbox) => { featureAPI.getFeaturesBbox(this.slug).then((featuresBbox) => {
if (bbox) { if (featuresBbox) {
mapUtil.getMap().fitBounds(bbox, { padding: [25, 25] }); if (featuresOffline.length > 0) {//* add offline features to BBOX with Turf
const allFeatures = [...featuresBbox.map((coordinates) => point(coordinates)), ...featuresOffline];
const featureCollect = featureCollection(allFeatures);
const newBbox = bbox(featureCollect);
if (newBbox) featuresBbox = [[newBbox[0], newBbox[1]], [newBbox[2], newBbox[3]]];
}
mapUtil.getMap().fitBounds(featuresBbox, { padding: [25, 25] });
} }
}); });
} }
...@@ -1254,7 +1376,7 @@ export default { ...@@ -1254,7 +1376,7 @@ export default {
margin-right: 5px; margin-right: 5px;
height: 25px; height: 25px;
vertical-align: bottom; vertical-align: bottom;
} }
.feature-type-container { .feature-type-container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
......
...@@ -100,8 +100,8 @@ ...@@ -100,8 +100,8 @@
PARAMÈTRES PARAMÈTRES
</div> </div>
<div class="four fields"> <div class="two fields">
<div class="field"> <!-- <div class="field">
<label for="archive_feature">Délai avant archivage</label> <label for="archive_feature">Délai avant archivage</label>
<div class="ui right labeled input"> <div class="ui right labeled input">
<input <input
...@@ -145,7 +145,7 @@ ...@@ -145,7 +145,7 @@
jour(s) jour(s)
</div> </div>
</div> </div>
</div> </div> -->
<div class="required field"> <div class="required field">
<label <label
for="access_level_pub_feature" for="access_level_pub_feature"
......