diff --git a/src/assets/resources/semantic-ui-2.4.2/semantic.min.css b/src/assets/resources/semantic-ui-2.4.2/semantic.min.css index 1d3cbd852930cbbb4c53884aad8bd27764e3ae0a..6bee5f9f9b143b29a5a91822c8ae366342b21993 100644 --- a/src/assets/resources/semantic-ui-2.4.2/semantic.min.css +++ b/src/assets/resources/semantic-ui-2.4.2/semantic.min.css @@ -24305,7 +24305,7 @@ ol.ui.list li[value]:before { } .ui.toggle.checkbox input:checked~.box:before, .ui.toggle.checkbox input:checked~label:before { - background-color: #ee2e24!important + background-color: #2185d0!important } .ui.toggle.checkbox input:checked~.box:after, .ui.toggle.checkbox input:checked~label:after { @@ -24319,7 +24319,7 @@ ol.ui.list li[value]:before { } .ui.toggle.checkbox input:focus:checked~.box:before, .ui.toggle.checkbox input:focus:checked~label:before { - background-color: #e90c00!important + background-color: #0d71bb!important } .ui.fitted.checkbox .box, .ui.fitted.checkbox label { diff --git a/src/assets/styles/openlayers-custom.css b/src/assets/styles/openlayers-custom.css index f086a7494e92c96c5ecc708c5ddb4e55c9c3d2cf..1a59628338578c994f05a0590327fe26a4debaa5 100644 --- a/src/assets/styles/openlayers-custom.css +++ b/src/assets/styles/openlayers-custom.css @@ -7,7 +7,7 @@ .ol-popup { position: absolute; background-color: white; - padding: 15px; + padding: 15px 5px 15px 15px; border-radius: 10px; bottom: 12px; left: -120px; @@ -23,7 +23,8 @@ font-size: .95em; } .ol-popup #popup-content h4 { - margin-right: .5em; + margin-right: 15px; + margin-bottom: .5em; color: #cacaca; } .ol-popup #popup-content h4, @@ -34,6 +35,20 @@ .ol-popup #popup-content div { color: #434343; } +.ol-popup #popup-content .fields { + max-height: 200px; + overflow: scroll; + padding-right: 10px; +} +.ol-popup #popup-content .divider { + margin-bottom: 0; +} +.ol-popup #popup-content #customFields h5 { + max-height: 20; +} +.ol-popup #popup-content #customFields h5 { + margin: .5em 0; +} .ol-popup:after, .ol-popup:before { top: 100%; diff --git a/src/components/FeatureType/FeatureTypeCustomForm.vue b/src/components/FeatureType/FeatureTypeCustomForm.vue index 8e253961d0e641e0e3d5ef5c72bcf319ec69bee4..fe568ad48ead4957603dc6d4a28bdcad421f93e8 100644 --- a/src/components/FeatureType/FeatureTypeCustomForm.vue +++ b/src/components/FeatureType/FeatureTypeCustomForm.vue @@ -125,7 +125,7 @@ }}</label> <Dropdown :disabled="!form.label.value || !form.name.value" - :options="fieldTypeChoices" + :options="customFieldTypeChoices" :selected="selectedFieldType" :selection.sync="selectedFieldType" /> @@ -315,6 +315,7 @@ import { mapState, mapActions } from 'vuex'; import Sortable from 'sortablejs'; +import { customFieldTypeChoices } from '@/utils'; import Dropdown from '@/components/Dropdown.vue'; import CustomFormConditionalField from '@/components/FeatureType/CustomFormConditionalField.vue'; @@ -339,17 +340,6 @@ export default { data() { return { - fieldTypeChoices: [ - { name: 'Booléen', value: 'boolean' }, - { name: 'Chaîne de caractères', value: 'char' }, - { name: 'Date', value: 'date' }, - { name: 'Liste de valeurs', value: 'list' }, - { name: 'Liste de valeurs pré-enregistrées', value: 'pre_recorded_list' }, - { name: 'Liste à choix multiples', value: 'multi_choices_list' }, - { name: 'Nombre entier', value: 'integer' }, - { name: 'Nombre décimal', value: 'decimal' }, - { name: 'Texte multiligne', value: 'text' }, - ], form: { is_mandatory: { value: false, @@ -439,7 +429,7 @@ export default { selectedFieldType: { // getter get() { - const currentFieldType = this.fieldTypeChoices.find( + const currentFieldType = customFieldTypeChoices.find( (el) => el.value === this.form.field_type.value ); if (currentFieldType) { diff --git a/src/components/FeatureType/SymbologySelector.vue b/src/components/FeatureType/SymbologySelector.vue index 41c57f6d3537d56fa22ff883c6ea097aaef00316..0fc52c4ed9b47db6679d9fa5c0acdbad7a3e2306 100644 --- a/src/components/FeatureType/SymbologySelector.vue +++ b/src/components/FeatureType/SymbologySelector.vue @@ -1,9 +1,9 @@ <template> <div> <div class="three fields"> - <h4 :class="['field', {'row-title' : isDefault}]"> + <h5 class="field"> {{ title }} - </h4> + </h5> <div class="required inline field"> <label :for="form.color.id_for_label">{{ form.color.label }}</label> <input @@ -15,7 +15,10 @@ > </div> - <div v-if="geomType === 'polygon' || geomType === 'multipolygon'"> + <div + v-if="geomType === 'polygon' || geomType === 'multipolygon'" + class="field" + > <label>Opacité <span>(%)</span></label> <div class="range-container"> <input @@ -75,7 +78,7 @@ export default { props: { title: { type: String, - default: 'Symbologie par défault :' + default: 'Couleur par défault :' }, initColor: { type: String, @@ -115,12 +118,6 @@ export default { }; }, - computed: { - isDefault() { - return this.title === 'Symbologie par défault :'; - } - }, - watch: { form: { deep: true, @@ -167,21 +164,16 @@ export default { .fields { align-items: center; - justify-content: space-between; - margin-top: 3em !important; } -.row-title { - display: inline; - font-size: 1.4em; - font-weight: normal; - width: 33%; - text-align: left; - margin-left: 0.5em; +#customFieldSymbology .fields { + margin-left: 1em !important; + margin-bottom: 1em !important; } -.default { - margin-bottom: 2rem; +h5 { + font-weight: initial; + font-style: italic; } #couleur { diff --git a/src/components/Project/Detail/ProjectFeatureTypes.vue b/src/components/Project/Detail/ProjectFeatureTypes.vue index 68ece3432f22f0cdf1253c798dfd914d4796b6de..6753351266207558d3e73b4376d31179ee4fa743 100644 --- a/src/components/Project/Detail/ProjectFeatureTypes.vue +++ b/src/components/Project/Detail/ProjectFeatureTypes.vue @@ -135,7 +135,7 @@ isOnline " :to="{ - name: 'editer-symbologie-signalement', + name: 'editer-affichage-signalement', params: { slug_type_signal: type.slug }, }" class=" @@ -149,7 +149,7 @@ button-hover-orange tiny-margin " - data-tooltip="Éditer la symbologie du type de signalement" + data-tooltip="Éditer l'affichage du type de signalement" data-position="top center" data-variation="mini" > diff --git a/src/router/index.js b/src/router/index.js index 56953714fbab4848193e56d9fab1c6dcd82e8f04..cf898c0c6593030c9e9b9b53abb262a5a4551cb3 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -105,9 +105,9 @@ const routes = [ component: () => import('../views/FeatureType/FeatureTypeEdit.vue') }, { - path: `/${projectBase}/:slug/type-signalement/:slug_type_signal/symbologie/`, - name: 'editer-symbologie-signalement', - component: () => import('../views/FeatureType/FeatureTypeSymbology.vue') + path: `/${projectBase}/:slug/type-signalement/:slug_type_signal/affichage/`, + name: 'editer-affichage-signalement', + component: () => import('../views/FeatureType/FeatureTypeDisplay.vue') }, // * FEATURE { diff --git a/src/services/map-service.js b/src/services/map-service.js index b9c5b31cba22178a0f5428947f2d93fd06ecab41..128ba6d6c1944b3293d81519f995020af4afcf18 100644 --- a/src/services/map-service.js +++ b/src/services/map-service.js @@ -631,6 +631,28 @@ const mapService = { this.map.on(eventName, callback); }, + createCustomFiedsContent(featureType, feature) { + const { customfield_set } = featureType; + if (!customfield_set) return ''; + const rowTemplate = (customfield) => { + const { label, name } = customfield; + const value = feature.getProperties()[name]; + return label && value !== undefined ? `<div class="customField-row">${label}: ${value}</div>` : ''; + }; + + let rows = ''; + for (const customField of customfield_set) { + rows += rowTemplate(customField); + } + + return rows.length > 0 ? + `<div id="customFields"> + <div class="ui divider"></div> + <h5>Champs personnalisés</h5> + ${rows} + </div>` : ''; + }, + _createContentPopup: function (feature) { const formatDate = (current_datetime) => { let formatted_date = current_datetime.getFullYear() + '-' + ('0' + (current_datetime.getMonth() + 1)).slice(-2) + '-' + ('0' + current_datetime.getDate()).slice(-2) + ' ' + @@ -641,6 +663,7 @@ const mapService = { if (feature.getProperties) { const properties = feature.getProperties(); + // ! "creator" doesn't exist in geojson, only creator_id is found, but we cannot retrieve the name in frontend's available data ({ status, updated_on, creator, index } = properties); // using parenthesis to allow destructuring object without declaration if (this.featureTypes) { featureType = feature.getProperties().feature_type || @@ -676,20 +699,22 @@ const mapService = { } const title = feature.getProperties ? feature.getProperties().title : feature.title; - const html = ` - <h4> + const html = `<h4> <a id="goToFeatureDetail" class="pointer">${title}</a> </h4> - <div> - Statut : ${status} - </div> - <div> - Type : ${featureType ? '<a id="goToFeatureTypeDetail" class="pointer">' + featureType.title + '</a>' : 'Type de signalement inconnu'} - </div> - <div> - Dernière mise à jour : ${updated_on} - </div> - ${author}`; + <div class="fields"> + <div> + Statut : ${status} + </div> + <div> + Type : ${featureType ? '<a id="goToFeatureTypeDetail" class="pointer">' + featureType.title + '</a>' : 'Type de signalement inconnu'} + </div> + <div> + Dernière mise à jour : ${updated_on} + </div> + ${author} + ${this.createCustomFiedsContent(featureType, feature)} + </div>`; return { html, featureType, index }; }, diff --git a/src/store/modules/feature-type.store.js b/src/store/modules/feature-type.store.js index 0b65b9b00abafab30f82898baf35d7ceeeff94b5..7dc7832d29c5130f4b41a2ddda74b55897f8e720 100644 --- a/src/store/modules/feature-type.store.js +++ b/src/store/modules/feature-type.store.js @@ -167,11 +167,11 @@ const feature_type = { .catch((error) => error.response); }, - async SEND_FEATURE_SYMBOLOGY({ getters, rootState }, symbology) { + async SEND_FEATURE_DISPLAY_CONFIG({ getters, rootState }, displayConfig) { const data = { title: getters.feature_type.title, project: rootState.projects.project.slug, - ...symbology + ...displayConfig }; return axios .put(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}v2/feature-types/${getters.feature_type.slug}/`, data) diff --git a/src/utils/index.js b/src/utils/index.js index 828b903b5fb77ccfffc2f232b668f01f7b3c75e1..53bf9cebd18015f128973c3010e6aefa483ca36e 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -118,6 +118,27 @@ export const reservedKeywords = [ 'lon' ]; +export const customFieldTypeChoices = [ + { name: 'Booléen', value: 'boolean' }, + { name: 'Chaîne de caractères', value: 'char' }, + { name: 'Date', value: 'date' }, + { name: 'Liste de valeurs', value: 'list' }, + { name: 'Liste de valeurs pré-enregistrées', value: 'pre_recorded_list' }, + { name: 'Liste à choix multiples', value: 'multi_choices_list' }, + { name: 'Nombre entier', value: 'integer' }, + { name: 'Nombre décimal', value: 'decimal' }, + { name: 'Texte multiligne', value: 'text' }, +]; + +export const featureNativeFields = [ + { name: 'status', label: 'Statut', field_type: 'Champ GéoContrib' }, + { name: 'feature_type', label: 'Type', field_type: 'Champ GéoContrib' }, + { name: 'updated_on', label: 'Dernière mise à jour', field_type: 'Champ GéoContrib' }, + { name: 'created_on', label: 'Date de création', field_type: 'Champ GéoContrib' }, + { name: 'display_creator', label: 'Auteur', field_type: 'Champ GéoContrib' }, + { name: 'display_last_editor', label: 'Dernier éditeur', field_type: 'Champ GéoContrib' }, +]; + export function findXformValue(feature, customField) { if (!feature) return null; if (feature.properties) { diff --git a/src/views/FeatureType/FeatureTypeSymbology.vue b/src/views/FeatureType/FeatureTypeDisplay.vue similarity index 59% rename from src/views/FeatureType/FeatureTypeSymbology.vue rename to src/views/FeatureType/FeatureTypeDisplay.vue index 1e2088a95df6bd7a95292ffeecce87f8b96f00cd..190769328e2a20235fe3065d9da00af02317b9ba 100644 --- a/src/views/FeatureType/FeatureTypeSymbology.vue +++ b/src/views/FeatureType/FeatureTypeDisplay.vue @@ -1,16 +1,19 @@ <template> - <div> + <div id="displayCustomisation"> <div :class="{ active: loading }" class="ui inverted dimmer" > <div class="ui loader" /> </div> + <h1 v-if="project && feature_type"> - Éditer la symbologie du type de signalement "{{ feature_type.title }}" pour le + Modifier l'affichage sur la carte des signalements de type "{{ feature_type.title }}" pour le projet "{{ project.title }}" </h1> - <div class="fourteen wide column"> + + <div id="symbology"> + <h3>Symbologie</h3> <form id="form-symbology-edit" action="" @@ -20,7 +23,7 @@ > <SymbologySelector v-if="feature_type" - class="default" + id="default" :init-color="feature_type.color" :init-icon="feature_type.icon" :init-opacity="feature_type.opacity" @@ -30,7 +33,7 @@ <div class="ui divider" /> <div v-if="customizableFields.length > 0" - class="field" + class="fields inline" > <label id="customfield-select-label" @@ -43,52 +46,88 @@ :options="customizableFields" :selected="selectedCustomfield" :selection.sync="selectedCustomfield" + :clearable="true" /> </div> </div> <div v-if="selectedCustomfield" + id="customFieldSymbology" class="field" > - <div + <SymbologySelector v-for="option of selectedFieldOptions" + :id="option" :key="option" - > - <SymbologySelector - :id="option" - :title="option" - :init-color="feature_type.colors_style.value ? - feature_type.colors_style.value.colors[option] ? - feature_type.colors_style.value.colors[option].value : - feature_type.colors_style.value.colors[option] - : null - " - :init-icon="feature_type.colors_style.value ? - feature_type.colors_style.value.icons[option] : - null - " - :init-opacity="getOpacity(feature_type, option)" - :geom-type="feature_type.geom_type" - @set="setColorsStyle" - /> - </div> - <div class="ui divider" /> - </div> - <button - id="save-symbology" - class="ui teal icon button margin-25" - type="button" - :disabled="!canSaveSymbology" - @click="sendFeatureSymbology" - > - <i - class="white save icon" - aria-hidden="true" + :title="option" + :init-color="feature_type.colors_style.value ? + feature_type.colors_style.value.colors[option] ? + feature_type.colors_style.value.colors[option].value : + feature_type.colors_style.value.colors[option] + : null + " + :init-icon="feature_type.colors_style.value ? + feature_type.colors_style.value.icons[option] : + null + " + :init-opacity="getOpacity(feature_type, option)" + :geom-type="feature_type.geom_type" + @set="setColorsStyle" /> - Sauvegarder la symbologie du type de signalement - </button> + </div> </form> </div> + + <div class="ui divider" /> + + <div + v-if="feature_type && feature_type.customfield_set" + id="popupDisplay" + > + <h3>Prévisualisation des champs personnalisés de l'info-bulle</h3> + <table class="ui definition single line compact table"> + <thead> + <tr> + <th><!-- Sélection --></th> + <th>Champ</th> + <th>Type</th> + </tr> + </thead> + <tbody> + <tr + v-for="field in featureAnyFields" + :key="field.name" + > + <td class="collapsing"> + <div class="ui toggle checkbox"> + <input + :checked="form.displayed_fields.includes(field.name)" + type="checkbox" + @input="toggleDisplay($event, field.name)" + > + <label /> + </div> + </td> + <td>{{ field.label }}</td> + <td>{{ field.field_type || getCustomFieldType(field.field_type) }}</td> + </tr> + </tbody> + </table> + </div> + + <button + id="save-display" + class="ui teal icon button margin-25" + type="button" + :disabled="!canSaveDisplayConfig" + @click="sendDisplayConfig" + > + <i + class="white save icon" + aria-hidden="true" + /> + Sauvegarder l'affichage du type de signalement + </button> </div> </template> @@ -97,12 +136,13 @@ import { isEqual } from 'lodash'; import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'; +import { featureNativeFields, customFieldTypeChoices } from '@/utils'; import SymbologySelector from '@/components/FeatureType/SymbologySelector.vue'; import Dropdown from '@/components/Dropdown.vue'; export default { - name: 'FeatureTypeSymbology', + name: 'FeatureTypeDisplay', components: { SymbologySelector, @@ -127,8 +167,9 @@ export default { opacities: {}, } }, + displayed_fields: ['status', 'feature_type', 'updated_on'] }, - canSaveSymbology: false + canSaveDisplayConfig: false }; }, @@ -147,7 +188,7 @@ export default { if (this.feature_type) { let options = this.feature_type.customfield_set.filter(el => el.field_type === 'list' || el.field_type === 'char' || el.field_type === 'boolean'); options = options.map((el) => { - return { name: [el.name, this.getFieldLabel(el.field_type)], value: el }; + return { name: [el.name, this.getCustomFieldType(el.field_type)], value: el }; }); return options; } @@ -171,10 +212,13 @@ export default { return this.form.colors_style.custom_field_name; }, set(newValue) { - if (newValue && newValue.value) { - this.form.colors_style.custom_field_name = newValue.value.name; + if (newValue !== undefined) { + this.form.colors_style.custom_field_name = newValue.value ? newValue.value.name : null; } } + }, + featureAnyFields() { + return [...featureNativeFields, ...this.feature_type.customfield_set]; } }, @@ -196,11 +240,13 @@ export default { if (isEqual(newValue, { color: this.feature_type.color, icon: this.feature_type.icon, - colors_style: this.feature_type.colors_style + opacity: this.feature_type.opacity, + colors_style: this.feature_type.colors_style, + displayed_fields: this.feature_type.displayed_fields })) { - this.canSaveSymbology = false; + this.canSaveDisplayConfig = false; } else { - this.canSaveSymbology = true; + this.canSaveDisplayConfig = true; } } } @@ -219,6 +265,7 @@ export default { this.GET_PROJECT_FEATURE_TYPES(this.$route.params.slug) .then(() => { this.initForm(); + // TODO : Use the global loader and get rid of this redondant loader this.loading = false; }) .catch(() => { @@ -232,14 +279,14 @@ export default { 'SET_CURRENT_FEATURE_TYPE_SLUG' ]), ...mapActions('feature-type', [ - 'SEND_FEATURE_SYMBOLOGY', + 'SEND_FEATURE_DISPLAY_CONFIG', 'GET_PROJECT_FEATURE_TYPES' ]), ...mapActions('projects', [ 'GET_PROJECT', 'GET_PROJECT_INFO', ]), - + initForm() { this.form.color = JSON.parse(JSON.stringify(this.feature_type.color)); //? wouldn't be better to use lodash: https://medium.com/@pmzubar/why-json-parse-json-stringify-is-a-bad-practice-to-clone-an-object-in-javascript-b28ac5e36521 this.form.icon = JSON.parse(JSON.stringify(this.feature_type.icon)); //? since the library is already imported ? @@ -251,10 +298,15 @@ export default { this.form.colors_style.value['opacities'] = {}; } if (this.feature_type.colors_style && Object.keys(this.feature_type.colors_style.colors).length > 0) { - this.selectedCustomfield = - this.feature_type.customfield_set.find( - el => el.name === this.feature_type.colors_style.custom_field_name - ).name; + const coloredCustomField = this.feature_type.customfield_set.find( + el => el.name === this.feature_type.colors_style.custom_field_name + ); + if (coloredCustomField) { + this.selectedCustomfield = coloredCustomField.name; + } + } + if (this.feature_type && this.feature_type.displayed_fields) { + this.form.displayed_fields = [...this.feature_type.displayed_fields]; } }, @@ -276,9 +328,17 @@ export default { this.form.colors_style.value.opacities[name] = opacity; //? why do we need to duplicate values ? for MVT ? }, - sendFeatureSymbology() { + toggleDisplay(evt, name) { + if (evt.target.checked) { + this.form.displayed_fields.push(name); + } else { + this.form.displayed_fields = this.form.displayed_fields.filter(el => el !== name); + } + }, + + sendDisplayConfig() { this.loading = true; - this.SEND_FEATURE_SYMBOLOGY(this.form) + this.SEND_FEATURE_DISPLAY_CONFIG(this.form) .then(() => { this.loading = false; this.$router.push({ @@ -306,37 +366,57 @@ export default { return null; }, - getFieldLabel(fieldType) { - switch (fieldType) { - case 'list': - return'Liste de valeurs'; - case 'char': - return 'Chaîne de caractères'; - case 'boolean': - return 'Booléen'; - } + getCustomFieldType(fieldType) { + return customFieldTypeChoices.find(el => el.value === fieldType).name; } } }; </script> <style lang="less" scoped> - -h1 { - margin-top: 1em; +#displayCustomisation { + h1 { + margin-top: 1em; + } + form { + text-align: left; + margin-left: 1em; + #customfield-select-label { + font-weight: 600; + font-size: 1.1em; + } + #custom_types-dropdown { + margin: 1em; + && > .dropdown { + width: 50%; + } + } + } } -form { - text-align: left; - #customfield-select-label { - font-weight: 600; - font-size: 1.1em; - } - #custom_types-dropdown > .dropdown { - width: 50%; +#symbology, #popupDisplay { + padding: 1.5em 0; + // shrink toggle background width and height + .ui.toggle.checkbox .box::after, .ui.toggle.checkbox label::after { + height: 15px; + width: 15px; + } + .ui.toggle.checkbox .box, .ui.toggle.checkbox label { + padding-left: 2.5rem; + } + // reduce toggle button width and height + .ui.toggle.checkbox .box::before, .ui.toggle.checkbox label::before { + height: 15px; + width: 35px; + } + // adjust toggled button placement + .ui.toggle.checkbox input:checked ~ .box::after, .ui.toggle.checkbox input:checked ~ label::after { + left: 20px; + } + .ui.toggle.checkbox .box, .ui.toggle.checkbox label, .ui.toggle.checkbox { + min-height: 15px; } - } </style>