diff --git a/package.json b/package.json index a40f1ac863f52abd4e70275af3bf37f063fc9626..9246b35aff41e8e1823ddc5d03b1de3eb68f3fb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geocontrib-frontend", - "version": "3.0.1", + "version": "3.0.2", "private": true, "scripts": { "serve": "npm run init-proxy & npm run init-serve", diff --git a/src/App.vue b/src/App.vue index e69824047105b97d90a13045d55c99e14c1611cc..1407a94c71a83f1ea2eee96d9dab62ba4854296a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -141,7 +141,7 @@ </div> </div> </div> - <div class="desktop flex push-right-desktop item title"> + <div class="desktop flex push-right-desktop item title abstract"> <span> {{ APPLICATION_ABSTRACT }} </span> @@ -405,7 +405,13 @@ footer { 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 { display: none !important; } @@ -420,7 +426,7 @@ footer { } } -@media screen and (max-width: 560px) { +@media screen and (max-width: 590px) { .desktop { display: none !important; } diff --git a/src/components/Dropdown.vue b/src/components/Dropdown.vue index d658cb72f7df541516c86220855f4c620e50d6bc..0fa9ba3d802b323bae63aa1286874cd830e7a27e 100644 --- a/src/components/Dropdown.vue +++ b/src/components/Dropdown.vue @@ -125,11 +125,10 @@ export default { methods: { toggleDropdown(val) { - if (this.isOpen) { - this.input = ''; // * clear input field when closing dropdown - } else if (this.search) { - //* focus on input if is a search dropdown - this.$refs.input.focus({ + if (this.isOpen) { //* if dropdown is open : + this.input = ''; // * -> clear input field when closing dropdown + } else if (this.search) { //* if dropdown is closed is a search dropdown: + this.$refs.input.focus({ //* -> focus on input field preventScroll: true, }); } else if (this.clearable && val.target && this.selected) { @@ -172,8 +171,9 @@ export default { }, clickOutsideDropdown(e) { - if (!e.target.closest(`#custom-dropdown${this.identifier}`)) + if (!e.target.closest(`#custom-dropdown${this.identifier}`) && this.isOpen) { this.toggleDropdown(false); + } }, }, }; diff --git a/src/service-worker.js b/src/service-worker.js index 7dd25fb94ea383cea55ef57465533c7422199f08..ae6b3aff91d2052812dc4111ff512321d3b33870 100644 --- a/src/service-worker.js +++ b/src/service-worker.js @@ -52,9 +52,9 @@ if (workbox) { }) ); workbox.routing.registerRoute( - /^https:\/\/osm\.geo2france\.fr\/mapcache/, + new RegExp('.*/service=WMS&request=GetMap/.*'), new workbox.strategies.CacheFirst({ - cacheName: 'mapcache', + cacheName: 'wms', plugins: [ new workbox.cacheableResponse.Plugin({ statuses: [0, 200], diff --git a/src/store/modules/feature.store.js b/src/store/modules/feature.store.js index 2a277dfd2aadc5ed78cbb27cf8fed02d9e0947b4..b9c90db26448e691b5777cad211d1e6b48458a36 100644 --- a/src/store/modules/feature.store.js +++ b/src/store/modules/feature.store.js @@ -168,7 +168,7 @@ const feature = { const cancelToken = axios.CancelToken.source(); 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}`; return axios .get(url, { cancelToken: cancelToken.token }) diff --git a/src/views/feature/Feature_detail.vue b/src/views/feature/Feature_detail.vue index e098b85c6bbfaae547ab7380e98e3ffce7cfe2cd..b081f3145bbdfd587c5b0c433fb60e59c02fa407 100644 --- a/src/views/feature/Feature_detail.vue +++ b/src/views/feature/Feature_detail.vue @@ -1,257 +1,245 @@ <template> <div v-frag> - <div - v-if="feature" - v-frag - > - <div class="row"> - <div class="fourteen wide column"> - <h1 class="ui header"> - <div class="content"> - {{ feature.title || feature.feature_id }} - <div class="ui icon right floated compact buttons"> - <router-link - v-if="permissions && permissions.can_create_feature" - :to="{ - name: 'ajouter-signalement', - params: { - slug_type_signal: $route.params.slug_type_signal, - }, - }" - class="ui button button-hover-orange" - data-tooltip="Ajouter un signalement" - data-position="bottom left" - > - <i class="plus fitted icon" /> - </router-link> - <router-link - v-if=" - (permissions && permissions.can_update_feature) || - isFeatureCreator || - isModerator - " - :to="{ - name: 'editer-signalement', - params: { - slug_signal: $route.params.slug_signal, - slug_type_signal: $route.params.slug_type_signal, - }, - }" - class="ui button button-hover-orange" - > - <i class="inverted grey pencil alternate icon" /> - </router-link> - <a - v-if="((permissions && permissions.can_update_feature) || isFeatureCreator) && isOnline" - id="feature-delete" - class="ui button button-hover-red" - @click="isCanceling = true" - > - <i class="inverted grey trash alternate icon" /> - </a> - </div> - <div class="ui hidden divider" /> - <div class="sub header prewrap"> - {{ feature.description }} - </div> + <div class="row"> + <div class="fourteen wide column"> + <h1 class="ui header"> + <div + v-if="feature" + class="content" + > + {{ feature.title || feature.feature_id }} + <div class="ui icon right floated compact buttons"> + <router-link + v-if="permissions && permissions.can_create_feature" + :to="{ + name: 'ajouter-signalement', + params: { + slug_type_signal: $route.params.slug_type_signal, + }, + }" + class="ui button button-hover-orange" + data-tooltip="Ajouter un signalement" + data-position="bottom left" + > + <i class="plus fitted icon" /> + </router-link> + <router-link + v-if=" + (permissions && permissions.can_update_feature) || + isFeatureCreator || + isModerator + " + :to="{ + name: 'editer-signalement', + params: { + slug_signal: $route.params.slug_signal, + slug_type_signal: $route.params.slug_type_signal, + }, + }" + class="ui button button-hover-orange" + > + <i class="inverted grey pencil alternate icon" /> + </router-link> + <a + v-if="((permissions && permissions.can_update_feature) || isFeatureCreator) && isOnline" + id="feature-delete" + class="ui button button-hover-red" + @click="isCanceling = true" + > + <i class="inverted grey trash alternate icon" /> + </a> </div> - </h1> - </div> + <div class="ui hidden divider" /> + <div class="sub header prewrap"> + {{ feature.description }} + </div> + </div> + </h1> </div> + </div> - <div class="row"> - <div class="seven wide column"> - <table class="ui very basic table"> - <tbody> - <div - v-for="(field, index) in feature.feature_data" - :key="'field' + index" - v-frag - > - <tr v-if="field"> - <td> - <b>{{ field.label }}</b> - </td> - <td> - <b> - <i - v-if="field.field_type === 'boolean'" - :class="[ - 'icon', - field.value ? 'olive check' : 'grey times', - ]" - /> - <span v-else> - {{ field.value }} - </span> - </b> - </td> - </tr> - </div> - <tr> - <td>Auteur</td> - <td>{{ feature.display_creator }}</td> - </tr> - <tr> - <td>Statut</td> + <div v-if="!feature && !loader.isLoading"> + Pas de signalement correspondant trouvé + </div> + + <div class="row"> + <div class="seven wide column"> + <table + v-if="feature" + class="ui very basic table" + > + <tbody> + <div + v-for="(field, index) in feature.feature_data" + :key="'field' + index" + v-frag + > + <tr v-if="field"> <td> - <i - v-if="feature.status" - :class="['icon', statusIcon]" - /> - {{ statusLabel }} + <b>{{ field.label }}</b> </td> - </tr> - <tr> - <td>Date de création</td> - <td v-if="feature.created_on"> - {{ feature.created_on | formatDate }} - </td> - </tr> - <tr> - <td>Date de dernière modification</td> - <td v-if="feature.updated_on"> - {{ feature.updated_on | formatDate }} - </td> - </tr> - </tbody> - </table> - - <h3>Liaison entre signalements</h3> - <table class="ui very basic table"> - <tbody> - <tr - v-for="(link, index) in linked_features" - :key="link.feature_to.title + index" - > - <td v-if="link.feature_to.feature_type_slug"> - {{ link.relation_type_display }} - <a @click="pushNgo(link)">{{ link.feature_to.title }} </a> - ({{ link.feature_to.display_creator }} - - {{ link.feature_to.created_on }}) + <td> + <b> + <i + v-if="field.field_type === 'boolean'" + :class="[ + 'icon', + field.value ? 'olive check' : 'grey times', + ]" + /> + <span v-else> + {{ field.value }} + </span> + </b> </td> </tr> - </tbody> - </table> - </div> + </div> + <tr> + <td>Auteur</td> + <td>{{ feature.display_creator }}</td> + </tr> + <tr> + <td>Statut</td> + <td> + <i + v-if="feature.status" + :class="['icon', statusIcon]" + /> + {{ statusLabel }} + </td> + </tr> + <tr> + <td>Date de création</td> + <td v-if="feature.created_on"> + {{ feature.created_on | formatDate }} + </td> + </tr> + <tr> + <td>Date de dernière modification</td> + <td v-if="feature.updated_on"> + {{ feature.updated_on | formatDate }} + </td> + </tr> + </tbody> + </table> + + <h3 v-if="feature"> + Liaison entre signalements + </h3> + <table + v-if="feature" + class="ui very basic table" + > + <tbody> + <tr + v-for="(link, index) in linked_features" + :key="link.feature_to.title + index" + > + <td v-if="link.feature_to.feature_type_slug"> + {{ link.relation_type_display }} + <a + class="pointer" + @click="pushNgo(link)" + >{{ link.feature_to.title }} </a> + ({{ link.feature_to.display_creator }} - + {{ link.feature_to.created_on }}) + </td> + </tr> + </tbody> + </table> + </div> - <div class="seven wide column"> - <div - id="map" - ref="map" - /> - </div> + <div class="seven wide column"> + <div + id="map" + ref="map" + /> </div> + </div> - <div class="row"> - <div class="seven wide column"> - <h2 class="ui header"> - Pièces jointes - </h2> + <div + v-if="feature" + class="row" + > + <div + class="seven wide column" + > + <h2 class="ui header"> + Pièces jointes + </h2> - <div - v-for="pj in attachments" - :key="pj.id" - class="ui divided items" - > - <div class="item"> + <div + v-for="pj in attachments" + :key="pj.id" + class="ui divided items" + > + <div class="item"> + <a + class="ui tiny image" + target="_blank" + :href="pj.attachment_file" + > + <img + :src=" + pj.extension === '.pdf' + ? require('@/assets/img/pdf.png') + : pj.attachment_file + " + > + </a> + <div class="middle aligned content"> <a - class="ui tiny image" + class="header" target="_blank" :href="pj.attachment_file" - > - <img - :src=" - pj.extension === '.pdf' - ? require('@/assets/img/pdf.png') - : pj.attachment_file - " - > - </a> - <div class="middle aligned content"> - <a - class="header" - target="_blank" - :href="pj.attachment_file" - >{{ - pj.title - }}</a> - <div class="description"> - {{ pj.info }} - </div> + >{{ + pj.title + }}</a> + <div class="description"> + {{ pj.info }} </div> </div> </div> - <i - v-if="attachments.length === 0" - >Aucune pièce jointe associée au signalement.</i> </div> + <i + v-if="attachments.length === 0" + >Aucune pièce jointe associée au signalement.</i> + </div> - <div class="seven wide column"> - <h2 class="ui header"> - Activité et commentaires - </h2> + <div class="seven wide column"> + <h2 class="ui header"> + Activité et commentaires + </h2> + <div + id="feed-event" + class="ui feed" + > <div - id="feed-event" - class="ui feed" + v-for="(event, index) in events" + :key="'event' + index" + v-frag > <div - v-for="(event, index) in events" - :key="'event' + index" + v-if="event.event_type === 'create'" v-frag > <div - v-if="event.event_type === 'create'" - v-frag + v-if="event.object_type === 'feature'" + class="event" > - <div - v-if="event.object_type === 'feature'" - class="event" - > - <div class="content"> - <div class="summary"> - <div class="date"> - {{ event.created_on }} - </div> - Création du signalement - <span v-if="user">par {{ event.display_user }}</span> - </div> - </div> - </div> - <div - v-else-if="event.object_type === 'comment'" - class="event" - > - <div class="content"> - <div class="summary"> - <div class="date"> - {{ event.created_on }} - </div> - Commentaire - <span v-if="user">par {{ event.display_user }}</span> - </div> - <div class="extra text"> - {{ event.related_comment.comment }} - <div - v-if="event.related_comment.attachment" - v-frag - > - <br><a - :href=" - DJANGO_BASE_URL + - event.related_comment.attachment.url - " - target="_blank" - ><i class="paperclip fitted icon" /> - {{ event.related_comment.attachment.title }}</a> - </div> + <div class="content"> + <div class="summary"> + <div class="date"> + {{ event.created_on }} </div> + Création du signalement + <span v-if="user">par {{ event.display_user }}</span> </div> </div> </div> <div - v-else-if="event.event_type === 'update'" + v-else-if="event.object_type === 'comment'" class="event" > <div class="content"> @@ -259,136 +247,160 @@ <div class="date"> {{ event.created_on }} </div> - Signalement mis à jour + Commentaire <span v-if="user">par {{ event.display_user }}</span> </div> + <div class="extra text"> + {{ event.related_comment.comment }} + <div + v-if="event.related_comment.attachment" + v-frag + > + <br><a + :href=" + DJANGO_BASE_URL + + event.related_comment.attachment.url + " + target="_blank" + ><i class="paperclip fitted icon" /> + {{ event.related_comment.attachment.title }}</a> + </div> + </div> </div> </div> </div> - </div> - - <div - v-if="permissions && permissions.can_create_feature && isOnline" - class="ui segment" - > - <form - id="form-comment" - class="ui form" + <div + v-else-if="event.event_type === 'update'" + class="event" > - <div class="required field"> - <label - :for="comment_form.comment.id_for_label" - >Ajouter un commentaire</label> - <ul - v-if="comment_form.comment.errors" - class="errorlist" - > - <li> - {{ comment_form.comment.errors }} - </li> - </ul> - <textarea - v-model="comment_form.comment.value" - :name="comment_form.comment.html_name" - rows="2" - /> - </div> - <label>Pièce jointe (facultative)</label> - <div class="two fields"> - <div class="field"> - <label - class="ui icon button" - for="attachment_file" - > - <i class="paperclip icon" /> - <span class="label">{{ - comment_form.attachment_file.value - ? comment_form.attachment_file.value - : "Sélectionner un fichier ..." - }}</span> - </label> - <input - id="attachment_file" - type="file" - accept="application/pdf, image/jpeg, image/png" - style="display: none" - name="attachment_file" - @change="onFileChange" - > - </div> - <div class="field"> - <input - id="title" - v-model="comment_form.attachment_file.title" - type="text" - name="title" - > - {{ comment_form.attachment_file.errors }} + <div class="content"> + <div class="summary"> + <div class="date"> + {{ event.created_on }} + </div> + Signalement mis à jour + <span v-if="user">par {{ event.display_user }}</span> </div> </div> + </div> + </div> + </div> + + <div + v-if="permissions && permissions.can_create_feature && isOnline" + class="ui segment" + > + <form + id="form-comment" + class="ui form" + > + <div class="required field"> + <label + :for="comment_form.comment.id_for_label" + >Ajouter un commentaire</label> <ul - v-if="comment_form.attachment_file.errors" + v-if="comment_form.comment.errors" class="errorlist" > <li> - {{ comment_form.attachment_file.errors }} + {{ comment_form.comment.errors }} </li> </ul> - <button - type="button" - class="ui compact green icon button" - @click="postComment" - > - <i class="plus icon" /> Poster le commentaire - </button> - </form> - </div> - </div> - </div> - - <div - v-if="isCanceling" - class="ui dimmer modals page transition visible active" - style="display: flex !important" - > - <div - :class="[ - 'ui mini modal subscription', - { 'active visible': isCanceling }, - ]" - > - <i - class="close icon" - @click="isCanceling = false" - /> - <div class="ui icon header"> - <i class="trash alternate icon" /> - Supprimer le signalement - </div> - <div class="actions"> + <textarea + v-model="comment_form.comment.value" + :name="comment_form.comment.html_name" + rows="2" + /> + </div> + <label>Pièce jointe (facultative)</label> + <div class="two fields"> + <div class="field"> + <label + class="ui icon button" + for="attachment_file" + > + <i class="paperclip icon" /> + <span class="label">{{ + comment_form.attachment_file.value + ? comment_form.attachment_file.value + : "Sélectionner un fichier ..." + }}</span> + </label> + <input + id="attachment_file" + type="file" + accept="application/pdf, image/jpeg, image/png" + style="display: none" + name="attachment_file" + @change="onFileChange" + > + </div> + <div class="field"> + <input + id="title" + v-model="comment_form.attachment_file.title" + type="text" + name="title" + > + {{ comment_form.attachment_file.errors }} + </div> + </div> + <ul + v-if="comment_form.attachment_file.errors" + class="errorlist" + > + <li> + {{ comment_form.attachment_file.errors }} + </li> + </ul> <button type="button" - class="ui red compact fluid button" - @click="deleteFeature" + class="ui compact green icon button" + @click="postComment" > - Confirmer la suppression + <i class="plus icon" /> Poster le commentaire </button> - </div> + </form> </div> </div> </div> + <div - v-else - v-frag + v-if="isCanceling" + class="ui dimmer modals page transition visible active" + style="display: flex !important" > - Pas de signalement correspondant trouvé + <div + :class="[ + 'ui mini modal subscription', + { 'active visible': isCanceling }, + ]" + > + <i + class="close icon" + @click="isCanceling = false" + /> + <div class="ui icon header"> + <i class="trash alternate icon" /> + Supprimer le signalement + </div> + <div class="actions"> + <button + type="button" + class="ui red compact fluid button" + @click="deleteFeature" + > + Confirmer la suppression + </button> + </div> + </div> </div> </div> </template> <script> import frag from 'vue-frag'; -import { mapGetters, mapState, mapActions } from 'vuex'; +import { mapGetters, mapState, mapActions, mapMutations } from 'vuex'; import { formatStringDate } from '@/utils'; import { mapUtil } from '@/assets/js/map-util.js'; import featureAPI from '@/services/feature-api'; @@ -435,6 +447,7 @@ export default { 'user', 'USER_LEVEL_PROJECTS', 'isOnline', + 'loader', ]), ...mapState('projects', [ 'project' @@ -442,19 +455,15 @@ export default { ...mapGetters([ 'permissions', ]), - ...mapState('feature', [ - 'linked_features', - 'statusChoices' - ]), + ...mapState('feature', { + linked_features: 'linked_features', + statusChoices: 'statusChoices', + feature: 'current_feature', + }), DJANGO_BASE_URL() { return this.$store.state.configuration.VUE_APP_DJANGO_BASE; }, - feature() { - const result = this.$store.state.feature.current_feature; - return result; - }, - isFeatureCreator() { if (this.feature && this.user) { return this.feature.creator === this.user.id; @@ -503,33 +512,34 @@ export default { }, mounted() { - this.$store.commit('DISPLAY_LOADER', 'Recherche du signalement'); + this.DISPLAY_LOADER('Recherche du signalement'); + if (!this.project) { // Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh axios.all([ - this.$store - .dispatch('projects/GET_PROJECT', this.$route.params.slug), - this.$store - .dispatch('projects/GET_PROJECT_INFO', this.$route.params.slug), - this.$store.dispatch('feature/GET_PROJECT_FEATURE', { + this.GET_PROJECT(this.$route.params.slug), + this.GET_PROJECT_INFO(this.$route.params.slug), + this.GET_PROJECT_FEATURE({ project_slug: this.$route.params.slug, feature_id: this.$route.params.slug_signal })]) .then(() => { - this.$store.commit('DISCARD_LOADER'); + this.DISCARD_LOADER(); this.initMap(); - }); - } if (!this.feature || this.feature.feature_id !== this.$route.params.slug_signal) { - this.$store.dispatch('feature/GET_PROJECT_FEATURE', { + }) + .catch(() => this.DISCARD_LOADER()); + } else if (!this.feature || this.feature.feature_id !== this.$route.params.slug_signal) { + this.GET_PROJECT_FEATURE({ project_slug: this.$route.params.slug, feature_id: this.$route.params.slug_signal }) .then(() => { - this.$store.commit('DISCARD_LOADER'); + this.DISCARD_LOADER(); this.initMap(); - }); + }) + .catch(() => this.DISCARD_LOADER()); } else { - this.$store.commit('DISCARD_LOADER'); + this.DISCARD_LOADER(); this.initMap(); } }, @@ -539,11 +549,21 @@ export default { }, methods: { + ...mapMutations([ + 'DISCARD_LOADER', + 'DISPLAY_LOADER', + ]), + ...mapActions('projects', [ + 'GET_PROJECT', + 'GET_PROJECT_INFO' + ]), ...mapActions('feature', [ - 'GET_PROJECT_FEATURES' + 'GET_PROJECT_FEATURES', + 'GET_PROJECT_FEATURE' ]), pushNgo(link) { + this.DISPLAY_LOADER('Recherche du signalement'); this.$router.push({ name: 'details-signalement', params: { @@ -551,10 +571,18 @@ export default { slug_signal: link.feature_to.feature_id, }, }); + this.GET_PROJECT_FEATURE({ + project_slug: this.$route.params.slug, + feature_id: this.$route.params.slug_signal, + }) + .then(()=> { + this.addFeatureToMap(); + this.DISCARD_LOADER(); + }) + .catch(() => this.DISCARD_LOADER()); this.getFeatureEvents(); this.getFeatureAttachments(); this.getLinkedFeatures(); - this.addFeatureToMap(); }, validateForm() { @@ -749,13 +777,15 @@ export default { getFeatureEvents() { featureAPI .getFeatureEvents(this.$route.params.slug_signal) - .then((data) => (this.events = data)); + .then((data) => (this.events = data)) + .catch((err)=> console.error(err)); }, getFeatureAttachments() { featureAPI .getFeatureAttachments(this.$route.params.slug_signal) - .then((data) => (this.attachments = data)); + .then((data) => (this.attachments = data)) + .catch((err)=> console.error(err)); }, getLinkedFeatures() { @@ -763,7 +793,8 @@ export default { .getFeatureLinks(this.$route.params.slug_signal) .then((data) => this.$store.commit('feature/SET_LINKED_FEATURES', data) - ); + ) + .catch((err)=> console.error(err)); }, }, }; diff --git a/src/views/project/Project_detail.vue b/src/views/project/Project_detail.vue index 5d3dc7aee03d414b7e0e976519cfad9156f43717..297b52ab9d989e025e210c11b583492fdf12b0ef 100644 --- a/src/views/project/Project_detail.vue +++ b/src/views/project/Project_detail.vue @@ -590,8 +590,8 @@ <h3 class="ui header"> Paramètres du projet </h3> - <div class="ui five stackable cards"> - <div class="card"> + <div class="ui three stackable cards"> + <!-- <div class="card"> <div class="center aligned content"> <h4 class="ui center aligned icon header"> <i class="disabled grey archive icon" /> @@ -616,7 +616,7 @@ <div class="center aligned extra content"> {{ project.delete_feature }} jours </div> - </div> + </div> --> <div class="card"> <div class="content"> <h4 class="ui center aligned icon header"> diff --git a/src/views/project/Project_edit.vue b/src/views/project/Project_edit.vue index c7f277cacdb7253f51c29a61414a1ed975b6c453..b4b47d64cffd39c8685d8585a8c89e1c2fe6542a 100644 --- a/src/views/project/Project_edit.vue +++ b/src/views/project/Project_edit.vue @@ -100,8 +100,8 @@ PARAMÈTRES </div> - <div class="four fields"> - <div class="field"> + <div class="two fields"> + <!-- <div class="field"> <label for="archive_feature">Délai avant archivage</label> <div class="ui right labeled input"> <input @@ -145,7 +145,7 @@ jour(s) </div> </div> - </div> + </div> --> <div class="required field"> <label for="access_level_pub_feature"