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
Showing
with 3859 additions and 40500 deletions
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
\ No newline at end of file
Source diff could not be displayed: it is too large. Options to address this: view the blob.
......@@ -10,6 +10,7 @@
*/
 
@import url('https://fonts.googleapis.com/css?family=Roboto Condensed:400,700,400italic,700italic&subset=latin');
/*!
* # Semantic UI 2.4.2 - Reset
* http://github.com/semantic-org/semantic-ui/
......@@ -271,7 +272,7 @@ body {
overflow-x: hidden;
min-width: 320px;
background: #fff;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-size: 14px;
line-height: 1.4285em;
color: #252525;
......@@ -279,7 +280,7 @@ body {
}
 
h1, h2, h3, h4, h5 {
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
line-height: 1.28571429em;
margin: calc(2rem - .14285714em) 0 1rem;
font-weight: 700;
......@@ -420,7 +421,7 @@ body .ui.inverted::-webkit-scrollbar-thumb:hover {
vertical-align: baseline;
background: #e0e1e2 none;
color: rgba(0, 0, 0, .6);
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
margin: 0 .25em 0 0;
padding: .78571429em 1.5em .78571429em;
text-transform: none;
......@@ -2924,7 +2925,7 @@ body .ui.inverted::-webkit-scrollbar-thumb:hover {
}
 
.ui.teal.button, .ui.teal.buttons .button {
background-color: #00b5ad;
background-color: var(--primary-color, #00b5ad);
color: #fff;
text-shadow: none;
background-image: none
......@@ -2936,60 +2937,68 @@ body .ui.inverted::-webkit-scrollbar-thumb:hover {
}
 
.ui.teal.button:hover, .ui.teal.buttons .button:hover {
background-color: #009c95;
background-color: var(--primary-highlight-color, #009c95)
;
color: #fff;
text-shadow: none
}
 
.ui.teal.button:focus, .ui.teal.buttons .button:focus {
background-color: #008c86;
background-color: var(--primary-highlight-color, #008c86);
color: #fff;
text-shadow: none
}
 
.ui.teal.button:active, .ui.teal.buttons .button:active {
background-color: #00827c;
background: var(--primary-highlight-color, #00827c);
color: #fff;
text-shadow: none
}
 
.ui.teal.active.button, .ui.teal.button .active.button:active, .ui.teal.buttons .active.button, .ui.teal.buttons .active.button:active {
background-color: #009c95;
background-color: var(--primary-highlight-color, #009c95)
;
color: #fff;
text-shadow: none
}
 
.ui.basic.teal.button, .ui.basic.teal.buttons .button {
-webkit-box-shadow: 0 0 0 1px #00b5ad inset!important;
box-shadow: 0 0 0 1px #00b5ad inset!important;
color: #00b5ad!important
-webkit-box-shadow: 0 0 0 1px var(--primary-color, #00b5ad) inset!important;
box-shadow: 0 0 0 1px var(--primary-color, #00b5ad) inset!important;
color: var(--primary-color, #00b5ad)!important
}
 
.ui.basic.teal.button:hover, .ui.basic.teal.buttons .button:hover {
background: 0 0!important;
-webkit-box-shadow: 0 0 0 1px #009c95 inset!important;
box-shadow: 0 0 0 1px #009c95 inset!important;
color: #009c95!important
-webkit-box-shadow: 0 0 0 1px var(--primary-highlight-color, #009c95)
inset!important;
box-shadow: 0 0 0 1px var(--primary-highlight-color, #009c95)
inset!important;
color: var(--primary-highlight-color, #009c95)
!important
}
 
.ui.basic.teal.button:focus, .ui.basic.teal.buttons .button:focus {
background: 0 0!important;
-webkit-box-shadow: 0 0 0 1px #008c86 inset!important;
box-shadow: 0 0 0 1px #008c86 inset!important;
color: #009c95!important
-webkit-box-shadow: 0 0 0 1px var(--primary-highlight-color, #008c86) inset!important;
box-shadow: 0 0 0 1px var(--primary-highlight-color, #008c86) inset!important;
color: var(--primary-highlight-color, #009c95)
!important
}
 
.ui.basic.teal.active.button, .ui.basic.teal.buttons .active.button {
background: 0 0!important;
-webkit-box-shadow: 0 0 0 1px #009c95 inset!important;
box-shadow: 0 0 0 1px #009c95 inset!important;
color: #00827c!important
-webkit-box-shadow: 0 0 0 1px var(--primary-highlight-color, #009c95)
inset!important;
box-shadow: 0 0 0 1px var(--primary-highlight-color, #009c95)
inset!important;
color: var(--primary-highlight-color, #00827c)!important
}
 
.ui.basic.teal.button:active, .ui.basic.teal.buttons .button:active {
-webkit-box-shadow: 0 0 0 1px #00827c inset!important;
box-shadow: 0 0 0 1px #00827c inset!important;
color: #00827c!important
-webkit-box-shadow: 0 0 0 1px var(--primary-highlight-color, #00827c) inset!important;
box-shadow: 0 0 0 1px var(--primary-highlight-color, #00827c) inset!important;
color: var(--primary-highlight-color, #00827c)!important
}
 
.ui.buttons:not(.vertical)>.basic.teal.button:not(:first-child) {
......@@ -3905,7 +3914,7 @@ body .ui.inverted::-webkit-scrollbar-thumb:hover {
}
 
.ui.text.container {
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
max-width: 700px!important;
line-height: 1.5
}
......@@ -5146,7 +5155,7 @@ i.flag.zimbabwe:before, i.flag.zw:before {
border: none;
margin: calc(2rem - .14285714em) 0 1rem;
padding: 0 0;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-weight: 700;
line-height: 1.28571429em;
text-transform: none;
......@@ -5516,15 +5525,16 @@ a.ui.inverted.green.header:hover {
}
 
.ui.teal.header {
color: #00b5ad!important
color: var(--primary-color, #00b5ad)!important
}
 
a.ui.teal.header:hover {
color: #009c95!important
color: var(--primary-highlight-color, #009c95)
!important
}
 
.ui.teal.dividing.header {
border-bottom: 2px solid #00b5ad
border-bottom: 2px solid var(--primary-color, #00b5ad)
}
 
.ui.inverted.teal.header {
......@@ -6027,7 +6037,7 @@ i.inverted.bordered.green.icon, i.inverted.circular.green.icon {
}
 
i.teal.icon {
color: #00b5ad!important
color: var(--primary-color, #00b5ad)!important
}
 
i.inverted.teal.icon {
......@@ -6035,7 +6045,7 @@ i.inverted.teal.icon {
}
 
i.inverted.bordered.teal.icon, i.inverted.circular.teal.icon {
background-color: #00b5ad!important;
background-color: var(--primary-color, #00b5ad)!important;
color: #fff!important
}
 
......@@ -11654,7 +11664,7 @@ img.ui.image {
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
text-align: left;
line-height: 1.21428571em;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
padding: .67857143em 1em;
background: #fff;
border: 1px solid rgba(34, 36, 38, .15);
......@@ -12658,14 +12668,16 @@ a.ui.active.label:hover, a.ui.labels .active.label:hover {
}
 
.ui.teal.label, .ui.teal.labels .label {
background-color: #00b5ad!important;
border-color: #00b5ad!important;
background-color: var(--primary-color, #00b5ad)!important;
border-color: var(--primary-color, #00b5ad)!important;
color: #fff!important
}
 
.ui.teal.labels .label:hover, a.ui.teal.label:hover {
background-color: #009c95!important;
border-color: #009c95!important;
background-color: var(--primary-highlight-color, #009c95)
!important;
border-color: var(--primary-highlight-color, #009c95)
!important;
color: #fff!important
}
 
......@@ -12674,19 +12686,21 @@ a.ui.active.label:hover, a.ui.labels .active.label:hover {
}
 
.ui.teal.ribbon.label {
border-color: #00827c!important
border-color: var(--primary-highlight-color, #00827c)!important
}
 
.ui.basic.teal.label {
background: none #fff!important;
color: #00b5ad!important;
border-color: #00b5ad!important
color: var(--primary-color, #00b5ad)!important;
border-color: var(--primary-color, #00b5ad)!important
}
 
.ui.basic.teal.labels a.label:hover, a.ui.basic.teal.label:hover {
background-color: #fff!important;
color: #009c95!important;
border-color: #009c95!important
color: var(--primary-highlight-color, #009c95)
!important;
border-color: var(--primary-highlight-color, #009c95)
!important
}
 
.ui.blue.label, .ui.blue.labels .label {
......@@ -13236,7 +13250,7 @@ a.ui.basic.label:hover {
.ui.list .list>.item .header, .ui.list>.item .header {
display: block;
margin: 0;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-weight: 700;
color: #252525
}
......@@ -15298,11 +15312,11 @@ ol.ui.list li[value]:before {
}
 
.ui.teal.segment:not(.inverted) {
border-top: 2px solid #00b5ad!important
border-top: 2px solid var(--primary-color, #00b5ad)!important
}
 
.ui.inverted.teal.segment {
background-color: #00b5ad!important;
background-color: var(--primary-color, #00b5ad)!important;
color: #fff!important
}
 
......@@ -15627,7 +15641,7 @@ ol.ui.list li[value]:before {
}
 
.ui.steps .step .title {
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-size: 1.14285714em;
font-weight: 700
}
......@@ -16174,7 +16188,7 @@ ol.ui.list li[value]:before {
}
 
.ui.form input:not([type]), .ui.form input[type=date], .ui.form input[type=datetime-local], .ui.form input[type=email], .ui.form input[type=file], .ui.form input[type=number], .ui.form input[type=password], .ui.form input[type=search], .ui.form input[type=tel], .ui.form input[type=text], .ui.form input[type=time], .ui.form input[type=url] {
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
margin: 0;
outline: 0;
-webkit-appearance: none;
......@@ -17827,7 +17841,7 @@ ol.ui.list li[value]:before {
}
 
.ui.grid>.row>.teal.column, .ui.grid>.teal.column, .ui.grid>.teal.row {
background-color: #00b5ad!important;
background-color: var(--primary-color, #00b5ad)!important;
color: #fff
}
 
......@@ -18237,7 +18251,7 @@ ol.ui.list li[value]:before {
display: -ms-flexbox;
display: flex;
margin: 1rem 0;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
background: #fff;
font-weight: 400;
border: 1px solid rgba(34, 36, 38, .15);
......@@ -19355,8 +19369,8 @@ ol.ui.list li[value]:before {
}
 
.ui.menu .teal.active.item, .ui.teal.menu .active.item {
border-color: #00b5ad!important;
color: #00b5ad!important
border-color: var(--primary-color, #00b5ad)!important;
color: var(--primary-color, #00b5ad)!important
}
 
.ui.blue.menu .active.item, .ui.menu .blue.active.item {
......@@ -19391,13 +19405,13 @@ ol.ui.list li[value]:before {
 
.ui.inverted.menu {
border: 0 solid transparent;
background: #373636;
background: var(--primary-color, #373636);
-webkit-box-shadow: none;
box-shadow: none
}
 
.ui.inverted.menu .item, .ui.inverted.menu .item>a:not(.ui) {
background: 0 0;
background: var(--primary-color, 0 0);
color: rgba(255, 255, 255, .9)
}
 
......@@ -19429,7 +19443,7 @@ ol.ui.list li[value]:before {
}
 
.ui.inverted.menu .dropdown.item:hover, .ui.inverted.menu .link.item:hover, .ui.inverted.menu a.item:hover, .ui.link.inverted.menu .item:hover {
background: rgba(255, 255, 255, .08);
background: var(--primary-highlight-color, rgba(255, 255, 255, 0.08));
color: #fff
}
 
......@@ -19439,12 +19453,12 @@ ol.ui.list li[value]:before {
}
 
.ui.inverted.menu .link.item:active, .ui.inverted.menu a.item:active {
background: #757575;
background: var(--primary-color, #757575);
color: #fff
}
 
.ui.inverted.menu .active.item {
background: #757575;
background: var(--primary-highlight-color, rgba(255, 255, 255, 0.08));
color: #fff!important
}
 
......@@ -19462,7 +19476,7 @@ ol.ui.list li[value]:before {
}
 
.ui.inverted.menu .active.item:hover {
background: #757575;
background: var(--primary-highlight-color, #757575);
color: #fff!important
}
 
......@@ -19545,7 +19559,7 @@ ol.ui.list li[value]:before {
}
 
.ui.inverted.menu .teal.active.item, .ui.inverted.teal.menu {
background-color: #00b5ad
background-color: var(--primary-color, #00b5ad)
}
 
.ui.inverted.teal.menu .item:before {
......@@ -20019,10 +20033,10 @@ ol.ui.list li[value]:before {
padding: 1em 1.5em;
line-height: 1.4285em;
color: #252525;
-webkit-transition: opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease;
transition: opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease;
transition: opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease;
transition: opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease, -webkit-box-shadow .5s ease;
-webkit-transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease;
transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease;
transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease;
transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease, -webkit-box-shadow .5s ease;
border-radius: .07142857rem;
-webkit-box-shadow: 0 0 0 1px rgba(34, 36, 38, .22) inset, 0 0 0 0 transparent;
box-shadow: 0 0 0 1px rgba(34, 36, 38, .22) inset, 0 0 0 0 transparent
......@@ -20038,7 +20052,7 @@ ol.ui.list li[value]:before {
 
.ui.message .header {
display: block;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-weight: 700;
margin: -.14285714em 0 0 0
}
......@@ -21016,11 +21030,11 @@ ol.ui.list li[value]:before {
}
 
.ui.teal.table {
border-top: .2em solid #00b5ad
border-top: .2em solid var(--primary-color, #00b5ad)
}
 
.ui.inverted.teal.table {
background-color: #00b5ad!important;
background-color: var(--primary-color, #00b5ad)!important;
color: #fff!important
}
 
......@@ -21766,7 +21780,7 @@ ol.ui.list li[value]:before {
.ui.card>.content>.header, .ui.cards>.card>.content>.header {
display: block;
margin: '';
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
color: rgba(0, 0, 0, .85)
}
 
......@@ -22039,13 +22053,15 @@ ol.ui.list li[value]:before {
}
 
.ui.cards>.teal.card, .ui.teal.card, .ui.teal.cards>.card {
-webkit-box-shadow: 0 0 0 1px #d4d4d5, 0 2px 0 0 #00b5ad, 0 1px 3px 0 #d4d4d5;
box-shadow: 0 0 0 1px #d4d4d5, 0 2px 0 0 #00b5ad, 0 1px 3px 0 #d4d4d5
-webkit-box-shadow: 0 0 0 1px #d4d4d5, 0 2px 0 0 var(--primary-color, #00b5ad), 0 1px 3px 0 #d4d4d5;
box-shadow: 0 0 0 1px #d4d4d5, 0 2px 0 0 var(--primary-color, #00b5ad), 0 1px 3px 0 #d4d4d5
}
 
.ui.cards>.teal.card:hover, .ui.teal.card:hover, .ui.teal.cards>.card:hover {
-webkit-box-shadow: 0 0 0 1px #d4d4d5, 0 2px 0 0 #009c95, 0 1px 3px 0 #bcbdbd;
box-shadow: 0 0 0 1px #d4d4d5, 0 2px 0 0 #009c95, 0 1px 3px 0 #bcbdbd
-webkit-box-shadow: 0 0 0 1px #d4d4d5, 0 2px 0 0 var(--primary-highlight-color, #009c95)
, 0 1px 3px 0 #bcbdbd;
box-shadow: 0 0 0 1px #d4d4d5, 0 2px 0 0 var(--primary-highlight-color, #009c95)
, 0 1px 3px 0 #bcbdbd
}
 
.ui.blue.card, .ui.blue.cards>.card, .ui.cards>.blue.card {
......@@ -22994,7 +23010,7 @@ ol.ui.list li[value]:before {
.ui.items>.item>.content>.header {
display: inline-block;
margin: -.21425em 0 0;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-weight: 700;
color: rgba(0, 0, 0, .85)
}
......@@ -23341,7 +23357,7 @@ ol.ui.list li[value]:before {
}
 
.ui.statistic>.value, .ui.statistics .statistic>.value {
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-size: 4rem;
font-weight: 400;
line-height: 1em;
......@@ -23351,7 +23367,7 @@ ol.ui.list li[value]:before {
}
 
.ui.statistic>.label, .ui.statistics .statistic>.label {
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-size: 1em;
font-weight: 700;
color: #252525;
......@@ -23550,7 +23566,7 @@ ol.ui.list li[value]:before {
}
 
.ui.statistics .teal.statistic>.value, .ui.teal.statistic>.value, .ui.teal.statistics .statistic>.value {
color: #00b5ad
color: var(--primary-color, #00b5ad)
}
 
.ui.blue.statistic>.value, .ui.blue.statistics .statistic>.value, .ui.statistics .blue.statistic>.value {
......@@ -23744,7 +23760,7 @@ ol.ui.list li[value]:before {
 
.ui.accordion .title:not(.ui) {
padding: .5em 0;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-size: 1em;
color: #252525
}
......@@ -24305,7 +24321,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 +24335,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 {
......@@ -24605,7 +24621,7 @@ body.dimmable>.dimmer {
.ui.dropdown>.dropdown.icon {
position: relative;
width: auto;
font-size: .85714286em;
font-size: .9em;
margin: 0 0 0 1em
}
 
......@@ -24823,9 +24839,9 @@ select.ui.dropdown {
z-index: 3;
margin: -.78571429em;
padding: .91666667em;
opacity: .8;
-webkit-transition: opacity .5s ease;
transition: opacity .5s ease
color: #999;
-webkit-transition: transform .2s ease;
transition: transform .2s ease
}
 
.ui.compact.selection.dropdown {
......@@ -24942,7 +24958,7 @@ select.ui.dropdown {
}
 
.ui.active.selection.dropdown>.dropdown.icon, .ui.visible.selection.dropdown>.dropdown.icon {
opacity: '';
transform: rotate(180deg);
z-index: 3
}
 
......@@ -25976,7 +25992,7 @@ select.ui.dropdown {
 
.ui.modal>.header {
display: block;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
background: #fff;
margin: 0;
padding: 1.25rem 1.5rem;
......@@ -26066,14 +26082,9 @@ select.ui.dropdown {
.ui.modal>.actions {
background: #f9fafb;
padding: 1rem 1rem;
border-top: 1px solid rgba(34, 36, 38, .15);
text-align: right
}
 
.ui.modal .actions>.button {
margin-left: .75em
}
@media only screen and (max-width:767px) {
.ui.modal {
width: 95%;
......@@ -26598,7 +26609,7 @@ a.ui.nag {
 
.ui.popup>.header {
padding: 0;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-size: 1.14285714em;
line-height: 1.2;
font-weight: 700
......@@ -27569,7 +27580,7 @@ a.ui.nag {
}
 
.ui.teal.progress .bar {
background-color: #00b5ad
background-color: var(--primary-color, #00b5ad)
}
 
.ui.teal.inverted.progress .bar {
......@@ -27962,7 +27973,7 @@ a.ui.nag {
 
.ui.search>.results .result .title {
margin: -.14285714em 0 0;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-weight: 700;
font-size: 1em;
color: rgba(0, 0, 0, .85)
......@@ -27984,7 +27995,7 @@ a.ui.nag {
}
 
.ui.search>.results>.message .header {
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-size: 1rem;
font-weight: 700;
color: #252525
......@@ -28148,7 +28159,7 @@ a.ui.nag {
width: 100px;
white-space: nowrap;
background: 0 0;
font-family: 'Roboto Condensed', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif);
font-size: 1em;
padding: .4em 1em;
font-weight: 700;
......
/* ---------------------------------- */
/* HEADER */
/* APP */
/* ---------------------------------- */
header {
background: #373636;
body {
height: 100%;
width: 100%;
margin: 0;
}
#app {
position: relative;
min-height: 100vh;
/* keep the space for loader, before page contents are injected */
display: flex;
/* used to fix height on sticky header and footer */
flex-direction: column;
/* used to fix height on sticky header and footer */
}
#app-header {
position: sticky;
top: 0;
z-index: 1;
background: var(--header-color, #373636);
.ui.inverted.menu {
background: var(--header-color, #373636);
}
.item {
background: var(--header-color, #373636);
}
}
#app-content {
overflow: auto;
flex: 1 0 auto;
/* used to fix height on sticky header and footer */
min-height: 61px;
/* value by default of the header, defined here to be sync with anchor below, in order to keep the page stuck to top */
position: relative;
/* for anchor below */
}
#scroll-top-anchor {
position: absolute;
top: -61px;
visibility: hidden;
}
.page-content {
max-width: 1200px;
padding: 0 2em;
margin: 2em auto;
}
#map {
width: 100%;
height: 100%;
min-height: 250px;
touch-action: none;
/* workaround for modifying feature on mobile */
}
#app-footer {
overflow: hidden;
background-color: #464646;
text-align: center;
flex-shrink: 0;
/* used to fix height on sticky header and footer */
position: sticky;
bottom: 0;
z-index: 1000;
}
#app-footer .ui.text.menu {
min-height: 30px !important;
}
#app-footer .ui.text.menu .item {
color: #ffffff;
padding: 5px 14px;
}
#app-footer .ui.text.menu a.item:hover {
color: var(--primary-color, #008c86);
}
#app-footer .ui.text.menu .item:not(:first-child) {
border-left: 1px solid rgba(34, 36, 38, .15);
}
/* ---------------------------------- */
/* UTILS */
/* ---------------------------------- */
.inline {
display: inline;
}
.no-margin {
margin: 0 !important;
}
.margin-top {
margin-top: 1rem;
}
.margin-bottom {
margin-bottom: 1rem;
}
main {
padding: 2em 0em;
.tiny-margin {
margin: 0.1rem 0 0.1rem 0.1rem !important;
}
.tiny-margin-left {
margin-left: 0.1rem !important;
}
.ellipsis {
text-overflow: ellipsis;
overflow: hidden;
}
.nowrap {
white-space: nowrap;
}
.important-flex {
display: flex !important;
}
.pointer:hover {
cursor: pointer !important;
}
.dimmer-anchor {
position: relative;
}
.full-width {
width: 100%;
}
/* ---------------------------------- */
/* MAIN */
/* MAIN */
/* ---------------------------------- */
.button-hover-orange:hover {
background: #fbbd08 !important;
}
.button-hover-green:hover {
background: #5bba21 !important;
}
.button-hover-red:hover {
background: #ee2e24 !important;
}
.ui.button.button-hover-red:hover,
.ui.button.button-hover-red:hover i.icon,
.ui.button.button-hover-green:hover,
.ui.button.button-hover-green:hover i.icon {
color: #fff !important;
}
.ui.button.button-hover-red:hover i.icon,
.ui.button.button-hover-green:hover i.icon {
transition: all 0.5s ease !important;
}
.ui.header .content {
width: 100%;
}
.ui.horizontal.divider {
color: #1ab2b6!important;
color: var(--primary-color, #008c86) !important;
padding-top: 1.5em;
}
......@@ -37,7 +185,7 @@ main {
display: none;
}
.ui.dropdown .menu > .header {
.ui.dropdown .menu>.header {
font-size: 1em;
text-transform: none;
}
......@@ -47,23 +195,33 @@ main {
overflow: auto;
}
.ui.dropdown .menu.text-wrap > .item {
.ui.dropdown .menu.text-wrap>.item {
white-space: normal;
word-wrap: normal;
}
.ui.checkbox.disabled>input {
cursor: default !important;
}
/* Add basemap view */
#form-layers .ui.buttons{
margin-bottom: 1rem;
#form-layers button.button:not(:last-of-type) {
margin-right: 0.5em !important;
}
#form-layers .errorlist{
#form-layers .errorlist {
list-style: none;
padding-left: 0;
color: #9f3a38;
}
#form-layers .infoslist {
list-style: none;
padding-left: 0;
color: #38989f;
}
/* Fix semantic ui overflow when is too long */
.layer-item .form div.text {
width: 100%
......@@ -79,9 +237,9 @@ main {
}
/* Thicker borders for each basemap segment */
#form-layers [data-segments=basemap_set-SEGMENTS] > .ui.segment {
#form-layers [data-segments=basemap_set-SEGMENTS]>.ui.segment {
margin-bottom: 3rem;
border: 1px solid rgba(34,36,38,.30);
border: 1px solid rgba(34, 36, 38, .30);
}
......@@ -99,59 +257,202 @@ main {
opacity: 0.9;
}
/* */
/* ---------------------------------- */
/* LEAFLET DRAW TOOLBAR */
/* LEAFLET DRAW TOOLBAR */
/* ---------------------------------- */
.leaflet-draw-toolbar a.leaflet-draw-draw-circlemarker,
.leaflet-draw-toolbar a.leaflet-draw-draw-polyline,
.leaflet-draw-toolbar a.leaflet-draw-draw-polygon {
background-color: #FFA19E;
background-color: #FFA19E;
}
/* ---------------------------------- */
/* ERROR LIST */
/* ---------------------------------- */
.errorlist {
margin-top: 1rem;
padding: 0;
}
.infoslist {
margin-top: 0.1rem;
padding: 0;
}
.errorlist>li {
list-style: none;
color: rgb(177, 55, 55);
border: thin solid rgb(197, 157, 157);
border-radius: 3px;
background-color: rgb(250, 241, 242);
padding: 0.5rem 1rem;
}
.infoslist>li {
list-style: none;
color: #38989f;
border-radius: 3px;
padding: 0;
text-align: right;
}
/* ---------------------------------- */
/* LEAFLET*/
/* PAGINATION */
/* ---------------------------------- */
.leaflet-container {
background: #FFF;
.custom-pagination {
display: flex;
align-items: center;
list-style: none;
font-size: 1.2em;
}
.custom-pagination>.page-item>.page-link {
border: none;
font-weight: 400;
color: #008080;
}
.custom-pagination>.page-item.active>.page-link {
color: #008080;
background-color: transparent;
font-weight: bolder;
text-shadow: 0 0 2px #008080;
padding: 0.325em 0.75em;
pointer-events: none;
}
.custom-pagination>.page-item.disabled>.page-link {
opacity: 0.5;
pointer-events: none;
}
.custom-pagination>div>.page-item>.page-link {
border: none;
font-weight: 400;
color: #008080;
padding: 0.325em 0.75em;
}
.custom-pagination>div>.page-item.active>.page-link {
color: #008080;
background-color: transparent;
font-weight: bolder;
font-size: 1.2em;
text-shadow: 0 0 2px #008080;
padding: 0.325em 0.75em;
pointer-events: none;
}
.custom-pagination>div>.page-item.disabled>.page-link {
opacity: 0.5;
padding: 0.325em 0.75em;
pointer-events: none;
}
/* ---------------------------------- */
/* FOOTER */
/* MULTISELECT */
/* ---------------------------------- */
footer {
background-color: #464646;
.multiselect {
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif) !important;
}
.multiselect__tags {
border: 1px solid #ced4da;
border-radius: 0 !important;
font-family: var(--font-family, 'Roboto Condensed', 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif) !important;
font-size: 1rem !important;
}
.multiselect__tags>.multiselect__input {
border: none !important;
font-size: 1rem !important;
overflow: hidden;
text-overflow: ellipsis;
}
.multiselect__placeholder {
color: #838383;
margin-bottom: 0px;
padding-top: 0;
}
.multiselect__single,
.multiselect__tags,
.multiselect__content,
.multiselect__option {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 100%;
background-color: #fff;
font-size: 1rem !important;
}
.multiselect__select {
z-index: 1 !important;
}
.multiselect__content-wrapper {
box-shadow: 0 2px 3px 0 rgba(34, 36, 38, .15);
}
.multiselect__clear {
position: absolute;
right: 1px;
top: 8px;
width: 40px;
display: block;
cursor: pointer;
z-index: 9;
background-color: #fff;
padding: 0 4px;
text-align: center;
}
footer .ui.text.menu .item {
color: #ffffff;
padding: 5px 14px;
.multiselect__spinner {
z-index: 2 !important;
background-color: #fff;
opacity: 1;
top: 2px;
}
footer .ui.text.menu a.item:hover {
color: #1ab2b6;
.menu.projects>.item>.multiselect {
min-height: 0px !important;
}
footer .ui.text.menu .item:not(:first-child) {
border-left: 1px solid rgba(34,36,38,.15);
.menu.projects>.item>.multiselect>.multiselect__tags {
min-height: 0px !important;
}
.multiselect__option--selected {
background: #fff !important;
color: #35495e !important;
}
.multiselect__option--highlight {
background: #f3f3f3 !important;
color: #35495e !important;
}
.multiselect__option--selected.multiselect__option--highlight {
background: #f3f3f3 !important;
color: #35495e !important;
}
.multiselect__clear i.icon {
font-size: .75em;
color: #999;
margin: 0;
}
/* ---------------------------------- */
/* ERROR LIST */
/* OVERRIDE SEMANTIC STYLES */
/* ---------------------------------- */
.errorlist {
margin-top: 1rem;
padding: 0;
}
.errorlist > li {
list-style: none;
color: rgb(177, 55, 55);
border: thin solid rgb(197, 157, 157);
border-radius: 3px;
background-color: rgb(250, 241, 242);
padding: 1rem;
.ui.page.dimmer {
/* keep the dimmer above the dropdown (z-index 1001: above the map)*/
z-index: 1002;
}
\ No newline at end of file
/* OPENLAYERS */
.ol-zoom{
right: 5px !important;
left:unset !important;
}
.ol-popup {
position: absolute;
background-color: white;
padding: 15px 5px 15px 15px;
border-radius: 10px;
bottom: 12px;
left: -120px;
width: 240px;
line-height: 1.4;
-webkit-box-shadow: 0 3px 14px rgba(0,0,0,.4);
box-shadow: 0 3px 14px rgba(0,0,0,.4);
}
.ol-popup #popup-content {
line-height: 1.3;
font-size: .95em;
}
.ol-popup #popup-content h4 {
margin-right: 15px;
margin-bottom: .5em;
color: #cacaca;
}
.ol-popup #popup-content h4,
.ol-popup #popup-content div {
text-overflow: ellipsis;
overflow: hidden;
}
.ol-popup #popup-content div {
color: #434343;
}
.ol-popup #popup-content .fields {
max-height: 200px;
overflow-y: scroll;
overflow-x: hidden;
padding-right: 10px;
display: block; /* overide .ui.form.fields rule conflict in featureEdit page */
margin: 0; /* overide .ui.form.fields rule conflict in featureEdit page */
}
.ol-popup #popup-content .divider {
margin-bottom: 0;
}
.ol-popup #popup-content #customFields h5 {
max-height: 20;
margin: .5em 0;
}
.ol-popup:after, .ol-popup:before {
top: 100%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.ol-popup:after {
border-top-color: white;
border-width: 10px;
left: 50%;
margin-left: -10px;
}
.ol-popup:before {
border-top-color: #cccccc;
border-width: 11px;
left: 50%;
margin-left: -11px;
}
.ol-popup-closer {
text-decoration: none;
position: absolute;
top: 4px;
right: 0;
width: 24px;
height: 24px;
font: 18px/24px Tahoma,Verdana,sans-serif;
color: #757575;
}
.ol-popup-closer:after {
content: "×";
}
.ol-scale-line {
left: 2em;
background: hsla(0,0%,100%,.5);
padding: 0;
}
.ol-scale-line-inner {
color: #333;
font-size: 0.9em;
text-align: left;
padding-left: 0.5em;
border: 2px solid #777;
border-top: none;
}
.ol-control {
border: 2px solid rgba(0,0,0,.2);
background-clip: padding-box;
padding: 0;
}
.ol-control button {
background-color: #fff;
color: #000;
height: 30px;
width: 30px;
font: 700 18px Lucida Console,Monaco,monospace;
margin: 0;
}
.ol-control button:hover {
cursor: pointer;
background-color: #ebebeb;
}
.ol-control button:focus {
background-color: #ebebeb;
}
/* hide the popup before the map get loaded */
.map-container > #popup.ol-popup {
display: none;
}
.ol-full-screen {
top: calc(1em + 60px);
right: 5px !important;
}
/* Geolocation button */
div.geolocation-container {
position: absolute;
right: 6px;
z-index: 9;
border: 2px solid rgba(0,0,0,.2);
background-clip: padding-box;
padding: 0;
border-radius: 4px;
}
button.button-geolocation {
border: none;
padding: 0;
margin: 0;
text-align: center;
background-color: #fff;
color: rgb(39, 39, 39);
width: 30px;
height: 30px;
font: 700 18px Lucida Console,Monaco,monospace;
border-radius: 2px;
line-height: 1.15;
cursor: pointer;
}
button.button-geolocation:hover {
background-color: #ebebeb;
}
button.button-geolocation.tracking {
background-color: rgba(255, 145, 0, 0.904);
color: #fff;
}
button.button-geolocation i {
margin: 0;
vertical-align: top; /* strangely top is the only value that center at middle */
background-image: url(../img/geolocation-icon.png);
background-size: cover;
width: 25px;
height: 25px;
}
\ No newline at end of file
......@@ -13,8 +13,7 @@
border: 1px solid grey;
top: 0;
position: absolute;
/* Under this value, the map hide the sidebar */
z-index: 400;
z-index: 9;
}
.sidebar-layers {
......@@ -62,7 +61,7 @@
.sidebar-container.expanded .layers-icon svg path,
.sidebar-container.closing .layers-icon svg path {
fill: #00b5ad;
fill: var(--primary-color, #00b5ad);
}
@keyframes open-sidebar {
......@@ -132,7 +131,7 @@
}
.layers-icon:hover svg path {
fill: #00b5ad;
fill: var(--primary-color, #00b5ad);
}
.basemaps-title {
......@@ -144,7 +143,6 @@
}
/* Layer item */
.layer-item {
padding-bottom: 0.5rem;
}
......@@ -164,6 +162,7 @@
.range-container {
display: flex;
min-width: 15em; /* give space for the bubble since adding a min-width to keep its shape */
}
.range-output-bubble {
......@@ -172,6 +171,8 @@
padding: 4px 7px;
border-radius: 40px;
background-color: #2c3e50;
min-width: 2em;
text-align: center;
}
/* Overrides default padding of semantic-ui accordion */
......
import axios from 'axios';
axios.defaults.withCredentials = true;
// Add a request interceptor
axios.interceptors.request.use(function (config) {
config.headers['X-CSRFToken'] = (name => {
const re = new RegExp(name + '=([^;]+)');
const value = re.exec(document.cookie);
return (value != null) ? unescape(value[1]) : null;
})('csrftoken');
return config;
}, function (error) {
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
response.headers['X-CSRFToken'] = (name => {
const re = new RegExp(name + '=([^;]+)');
const value = re.exec(document.cookie);
return (value != null) ? unescape(value[1]) : null;
})('csrftoken');
return response;
}, function (error) {
return Promise.reject(error);
});
export default axios;
<template>
<div
id="user-activity"
class="ui stackable cards"
>
<!-- EVENTS -->
<div class="red card">
<div class="content">
<div class="center aligned header">
Mes dernières notifications reçues
</div>
<div class="center aligned description">
<div class="ui relaxed list">
<div
v-for="item in events"
:key="item.id"
class="item"
>
<div :class="['content', { 'ellipsis nowrap': item.related_feature.title }]">
{{ getNotificationName(item.event_type, item.object_type) }}
<div
v-if="item.object_type === 'project'"
>
<router-link
v-if="item.project_title"
:to="{
name: 'project_detail',
params: { slug: item.project_slug },
}"
>
{{ item.project_title }}
</router-link>
<span
v-else
class="meta"
><del>{{ item.project_slug }}</del>&nbsp;(supprimé)</span>
</div>
<div v-else>
<FeatureFetchOffsetRoute
v-if="item.related_feature.deletion_on === 'None'"
:feature-id="item.feature_id"
:properties="{
feature_type: {
slug: item.feature_type_slug
},
title: item.related_feature.title,
...item
}"
/>
<span
v-else
class="meta"
><del>{{ item.data.feature_title || item.feature_id }}</del>&nbsp;(supprimé)</span>
</div>
<div class="description">
<em>[ {{ item.created_on }}
<span v-if="user">
, par {{ item.display_user }}
</span>
]</em>
</div>
</div>
</div>
<em
v-if="!events || events.length === 0"
>Aucune notification pour le moment.</em>
</div>
</div>
</div>
</div>
<!-- FEATURES -->
<div class="orange card">
<div class="content">
<div class="center aligned header">
Mes derniers signalements
</div>
<div class="center aligned description">
<div class="ui relaxed list">
<div
v-for="item in features"
:key="item.id"
class="item"
>
<div class="content">
<div>
<FeatureFetchOffsetRoute
v-if="item.related_feature.deletion_on === 'None'"
:feature-id="item.feature_id"
:properties="{
feature_type: {
slug: item.feature_type_slug
},
title: item.related_feature.title,
...item
}"
/>
<span
v-else
class="meta"
>
<del>{{ item.data.feature_title || item.feature_id }}</del>&nbsp;(supprimé)
</span>
</div>
<div class="description">
<em>[ {{ item.created_on }}
<span v-if="user">
, par {{ item.display_user }}
</span>
]</em>
</div>
</div>
</div>
<em
v-if="!features || features.length === 0"
>Aucun signalement pour le moment.</em>
</div>
</div>
</div>
</div>
<!-- COMMENTS -->
<div class="yellow card">
<div class="content">
<div class="center aligned header">
Mes derniers commentaires
</div>
<div class="center aligned description">
<div class="ui relaxed list">
<div
v-for="item in comments"
:key="item.id"
class="item"
>
<div class="content">
<div>
<FeatureFetchOffsetRoute
v-if="item.related_feature.deletion_on === 'None'"
:feature-id="item.feature_id"
:properties="{
feature_type: {
slug: item.feature_type_slug
},
title: quoteComment(item.data.comment),
...item
}"
/>
<span
v-else
class="meta"
>
<del>{{ item.data.comment }}</del>&nbsp;(supprimé)
</span>
</div>
<div class="description">
<em>[ {{ item.created_on }}
<span v-if="user">
, par {{ item.display_user }}
</span>
]</em>
</div>
</div>
</div>
<em
v-if="!comments || comments.length === 0"
>Aucun commentaire pour le moment.</em>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import miscAPI from '@/services/misc-api';
import FeatureFetchOffsetRoute from '@/components/Feature/FeatureFetchOffsetRoute';
export default {
name: 'UserActivity',
components: {
FeatureFetchOffsetRoute,
},
data() {
return {
events: [],
features: [],
comments: [],
};
},
computed: {
...mapState([
'user',
]),
isSharedProject() {
return this.$route.path.includes('projet-partage');
},
},
created(){
this.getEvents();
// unset project to avoid interfering with generating query in feature links
this.$store.commit('projects/SET_PROJECT', null);
},
methods: {
getEvents(){
miscAPI.getUserEvents(this.$route.params.slug)
.then((data)=>{
this.events = data.events;
this.features = data.features;
this.comments = data.comments;
});
},
getNotificationName(eventType, objectType) {
if (eventType === 'create') {
if (objectType === 'feature') {
return 'Signalement créé';
} else if (objectType === 'comment') {
return 'Commentaire créé';
} else if (objectType === 'attachment') {
return 'Pièce jointe ajoutée';
} else if (objectType === 'project') {
return 'Projet créé';
}
} else if (eventType === 'update') {
if (objectType === 'feature') {
return 'Signalement mis à jour';
} else if (objectType === 'project') {
return 'Projet mis à jour';
}
} else if (eventType === 'delete') {
if (objectType === 'feature') {
return 'Signalement supprimé';
} else if (objectType === 'project') {
return 'Projet mis à jour';
} else {
return 'Événement inconnu';
}
}
},
quoteComment(comment) {
return `"${comment}"`;
},
}
};
</script>
<style scoped lang="less">
#user-activity {
flex-flow: column;
margin: 1em 0;
.card {
margin: .875em 0;
}
}
</style>
<template>
<div>
<h4 class="ui horizontal divider header">
PROFIL
</h4>
<div class="ui divided list">
<div class="item">
<div class="right floated content">
<div class="description">
<span v-if="user.username">{{ user.username }} </span>
</div>
</div>
<div class="content">
Nom d'utilisateur
</div>
</div>
<div class="item">
<div class="right floated content">
<div class="description">
{{ userFullname }}
</div>
</div>
<div class="content">
Nom complet
</div>
</div>
<div class="item">
<div class="right floated content">
<div class="description">
{{ user.email }}
</div>
</div>
<div class="content">
Adresse e-mail
</div>
</div>
<div class="item">
<div class="right floated content">
<div class="description">
{{ user.is_superuser ? "Oui" : "Non" }}
</div>
</div>
<div class="content">
Administrateur
</div>
</div>
</div>
<div
v-if="qrcode"
class="qrcode"
>
<img
:src="qrcode"
alt="qrcode"
>
<p>
Ce QR code vous permet de vous connecter à l'application mobile GéoContrib (bientôt disponible)
</p>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import QRCode from 'qrcode';
export default {
name: 'UserProfile',
data() {
return {
qrcode: null
};
},
computed: {
...mapState([
'configuration',
'user',
'userToken'
]),
userFullname() {
if (this.user.first_name || this.user.last_name) {
return `${this.user.first_name} ${this.user.last_name}`;
}
return null;
},
},
created() {
this.GET_USER_TOKEN()
.then(async () => {
try {
const qrcodeData = {
url: `${this.configuration.VUE_APP_DJANGO_BASE}/geocontrib/`,
token: this.userToken
};
this.qrcode = await QRCode.toDataURL(JSON.stringify(qrcodeData));
} catch (err) {
console.error(err);
}
})
.catch((err) => {
console.error(err);
});
},
methods: {
...mapActions([
'GET_USER_TOKEN'
])
}
};
</script>
<style scoped lang="less">
.qrcode {
img {
display: block;
margin: auto;
width: 12rem;
}
p {
font-size: 0.8rem;
font-style: italic;
text-align: center;
}
}
</style>
<template>
<div>
<h4 class="ui horizontal divider header">
MES PROJETS
</h4>
<div class="ui divided items">
<div
:class="['ui inverted dimmer', { active: projectsLoading }]"
>
<div class="ui text loader">
Récupération des projets en cours...
</div>
</div>
<div
v-for="project in projectsArray"
:key="project.slug"
class="item"
>
<div
v-if="user_permissions[project.slug].can_view_project"
class="item-content-wrapper"
>
<div class="ui tiny image">
<img
v-if="project.thumbnail"
class="ui small image"
alt="Thumbnail projet"
:src="
project.thumbnail.includes('default')
? require('@/assets/img/default.png')
: DJANGO_BASE_URL + project.thumbnail + refreshId()
"
height="200"
>
</div>
<div class="middle aligned content">
<router-link
:to="{
name: 'project_detail',
params: { slug: project.slug },
}"
class="header"
>
{{ project.title }}
</router-link>
<div class="description">
<p>{{ project.description }}</p>
</div>
<div class="meta top">
<span
class="right floated"
>
<strong>Projet {{ project.moderation ? "" : "non" }} modéré</strong>
</span>
<span>
Niveau d'autorisation requis : {{ project.access_level_pub_feature }}
</span><br>
<span>
Mon niveau d'autorisation :
<span v-if="USER_LEVEL_PROJECTS && project">
{{ USER_LEVEL_PROJECTS[project.slug] }}
</span>
<span v-if="user && user.is_administrator">
{{ "+ Gestionnaire métier" }}
</span>
</span>
</div>
<div class="meta">
<span
class="right floated"
:data-tooltip="`Projet créé le ${project.created_on}`"
>
<i
class="calendar icon"
aria-hidden="true"
/>
&nbsp;{{ project.created_on }}
</span>
<span data-tooltip="Membres">
{{ project.nb_contributors }}&nbsp;
<i
class="user icon"
aria-hidden="true"
/>
</span>
<span data-tooltip="Signalements publiés">
{{ project.nb_published_features }}&nbsp;
<i
class="map marker icon"
aria-hidden="true"
/>
</span>
<span data-tooltip="Commentaires">
{{ project.nb_published_features_comments }}&nbsp;
<i
class="comment icon"
aria-hidden="true"
/>
</span>
</div>
</div>
</div>
</div>
<!-- PAGINATION -->
<Pagination
v-if="count"
:nb-pages="Math.ceil(count/10)"
@page-update="changePage"
/>
</div>
</div>
</template>
<script>
import { mapState, mapMutations, mapActions } from 'vuex';
import Pagination from '@/components/Pagination.vue';
export default {
name: 'UserProjectList',
components: {
Pagination,
},
data() {
return {
projectsLoading: true,
};
},
computed: {
...mapState([
'user',
'USER_LEVEL_PROJECTS',
'user_permissions',
]),
// todo : filter projects to user
...mapState('projects', [
'projects',
'count',
]),
DJANGO_BASE_URL() {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
isSharedProject() {
return this.$route.path.includes('projet-partage');
},
availableProjects() {
if (this.isSharedProject) {
return this.projects.filter((el) => el.slug === this.$route.params.slug);
}
return this.projects;
},
projectsArray() { //* if only one project, only project object is returned
return Array.isArray(this.projects) ? this.projects : [this.projects];
}
},
created(){
this.SET_PROJECTS([]); //* empty previous project to avoid undefined user_permissions[project.slug]
this.getData();
},
methods: {
...mapMutations('projects', [
'SET_CURRENT_PAGE',
'SET_PROJECTS',
]),
...mapActions('projects', [
'GET_PROJECTS',
]),
refreshId() {
const crypto = window.crypto || window.msCrypto;
var array = new Uint32Array(1);
return '?ver=' + crypto.getRandomValues(array); // Compliant for security-sensitive use cases
},
getData(page) {
this.loading = true;
this.GET_PROJECTS({ ismyaccount: true, projectSlug: this.$route.params.slug, page })
.then(() => this.projectsLoading = false)
.catch(() => this.projectsLoading = false);
},
changePage(e) {
this.getData(e);
},
}
};
</script>
<style lang="less" scoped>
.ui.divided.items {
.item {
.item-content-wrapper {
width: 100%;
margin: 0;
padding: 1em 0;
display: flex;
.middle.aligned.content {
.header {
font-size: 1.28571429em;
font-weight: 600;
color: rgb(31, 31, 31)
}
}
}
}
> .item:nth-child(2) {
border: none !important;
}
}
.description {
p {
text-align: justify;
}
}
@media only screen and (min-width: 767px) {
.item-content-wrapper {
align-items: flex-start;
.middle.aligned.content {
width: 100%;
padding: 0 0 0 1.5em;
.meta.top {
span {
line-height: 1.2em;
}
}
}
}
}
@media only screen and (max-width: 767px) {
.item-content-wrapper {
flex-direction: column;
align-items: center;
.middle.aligned.content {
width: 80%;
padding: 1.5em 0 0;
.meta.top {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
.right.floated {
float: none !important;
margin-left: 0 !important;
margin-bottom: 0.5em;
}
span {
margin: 0.15em 0;
}
}
}
}
}
</style>
<template>
<div id="app-footer">
<div class="ui compact text menu">
<router-link
:to="{name: 'mentions'}"
class="item"
>
Mentions légales
</router-link>
<router-link
:to="{name: 'aide'}"
class="item"
>
Aide
</router-link>
<p class="item">
Version {{ PACKAGE_VERSION }}
</p>
</div>
</div>
</template>
<script>
export default {
name: 'AppFooter',
computed: {
PACKAGE_VERSION: () => process.env.PACKAGE_VERSION || '0',
}
};
</script>
<template>
<div
id="app-header"
:class="$route.name"
>
<div class="menu container">
<div class="ui inverted icon menu">
<router-link
:is="isSharedProject ? 'span' : 'router-link'"
:to="isSharedProject ? '' : '/'"
:class="['header item', {disable: isSharedProject}]"
>
<img
class="ui right spaced image"
alt="Logo de l'application"
:src="logo"
>
<span class="desktop">
{{ APPLICATION_NAME }}
</span>
</router-link>
<div
v-if="width <= 560 || (width > 560 && project)"
id="menu-dropdown"
:class="['ui dropdown item', { 'active visible': menuIsOpen }]"
@click="menuIsOpen = !menuIsOpen"
>
<div
v-if="!isOnline"
class="crossed-out mobile"
>
<i
class="wifi icon"
aria-hidden="true"
/>
</div>
<span class="expand-center">
<span v-if="project"> Projet : {{ project.title }} </span>
</span>
<i
class="dropdown icon"
aria-hidden="true"
/>
<div
:class="[
'menu dropdown-list transition',
{ 'visible': menuIsOpen },
]"
style="z-index: 401"
>
<router-link
v-if="project"
:to="{
name: 'project_detail',
params: { slug: project.slug },
}"
class="item"
>
<i
class="home icon"
aria-hidden="true"
/>Accueil
</router-link>
<router-link
v-if="project"
:to="{
name: 'liste-signalements',
params: { slug: project.slug },
}"
class="item"
>
<i
class="list icon"
aria-hidden="true"
/>Liste & Carte
</router-link>
<router-link
v-if="
project && isOnline && hasAdminRights"
:to="{
name: 'project_mapping',
params: { slug: project.slug },
}"
class="item"
>
<i
class="map icon"
aria-hidden="true"
/>Fonds cartographiques
</router-link>
<router-link
v-if="
project && isOnline && hasAdminRights"
:to="{
name: 'project_members',
params: { slug: project.slug },
}"
class="item"
>
<i
class="users icon"
aria-hidden="true"
/>Membres
</router-link>
<div class="mobile">
<router-link
:is="isOnline ? 'router-link' : 'span'"
v-if="user"
:to="{
name: 'my_account',
params: { slug: isSharedProject && $route.params.slug ? $route.params.slug : null }
}"
class="item"
>
{{ userFullname || user.username || "Utilisateur inconnu" }}
</router-link>
<div
v-if="USER_LEVEL_PROJECTS && project"
class="item ui label vertical no-hover"
>
<!-- super user rights are higher than others -->
{{ user && user.is_superuser ? 'Administrateur' : USER_LEVEL_PROJECTS[project.slug] }}
<br>
</div>
<div
v-if="user && user.is_administrator"
class="item ui label vertical no-hover"
>
Gestionnaire métier
</div>
<div
v-if="!DISABLE_LOGIN_BUTTON"
>
<a
v-if="user"
class="item"
@click="logout"
><i
class="ui logout icon"
aria-hidden="true"
/>
</a>
<router-link
v-else-if="!user && !SSO_LOGIN_URL"
:to="{ name : 'login' }"
class="item"
>
Se connecter
</router-link>
<a
v-else
class="item"
:href="SSO_LOGIN_URL"
target="_self"
>Se connecter</a>
</div>
</div>
</div>
</div>
<div class="desktop flex push-right-desktop item title abstract">
<span>
{{ APPLICATION_ABSTRACT }}
</span>
</div>
<div class="desktop flex push-right-desktop">
<div
v-if="!isOnline"
class="item network-icon"
>
<span
data-tooltip="Vous êtes hors-ligne,
vos changements pourront être envoyés au serveur au retour de la connexion"
data-position="bottom right"
>
<div class="crossed-out">
<i
class="wifi icon"
aria-hidden="true"
/>
</div>
</span>
</div>
<router-link
:is="isOnline ? 'router-link' : 'span'"
v-if="user"
:to="{
name: 'my_account',
params: { slug: isSharedProject && $route.params.slug ? $route.params.slug : null }
}"
class="item"
>
{{ userFullname || user.username || "Utilisateur inconnu" }}
</router-link>
<div
v-if="USER_LEVEL_PROJECTS && project"
class="item ui label vertical no-hover"
>
<!-- super user rights are higher than others -->
{{ user && user.is_superuser ? 'Administrateur' : USER_LEVEL_PROJECTS[project.slug] }}
<br>
</div>
<div
v-if="user && user.is_administrator"
class="item ui label vertical no-hover"
>
Gestionnaire métier
</div>
<div
v-if="!DISABLE_LOGIN_BUTTON"
>
<a
v-if="user"
class="item log-item"
@click="logout"
><i
class="ui logout icon"
aria-hidden="true"
/>
</a>
<router-link
v-else-if="!user && !SSO_LOGIN_URL"
:to="{ name : 'login' }"
class="item log-item"
>
Se Connecter
</router-link>
<a
v-else
class="item log-item"
:href="SSO_LOGIN_URL"
target="_self"
>Se connecter</a>
</div>
</div>
</div>
<MessageInfoList />
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import MessageInfoList from '@/components/MessageInfoList';
export default {
name: 'AppHeader',
components: {
MessageInfoList
},
data() {
return {
menuIsOpen: false,
width: window.innerWidth > 0 ? window.innerWidth : screen.width,
};
},
computed: {
...mapState([
'user',
'USER_LEVEL_PROJECTS',
'configuration',
'loader',
'isOnline'
]),
...mapState('projects', [
'projects',
'project',
]),
APPLICATION_NAME() {
return this.configuration.VUE_APP_APPLICATION_NAME;
},
APPLICATION_ABSTRACT() {
return this.configuration.VUE_APP_APPLICATION_ABSTRACT;
},
DISABLE_LOGIN_BUTTON() {
return this.configuration.VUE_APP_DISABLE_LOGIN_BUTTON;
},
SSO_LOGIN_URL() {
if (this.configuration.VUE_APP_LOGIN_URL) {
// add a next parameter with the pathname as expected by OGS to redirect after login
return `${this.configuration.VUE_APP_LOGIN_URL}/?next=${encodeURIComponent(window.location.pathname)}`;
}
return null;
},
logo() {
return this.configuration.VUE_APP_LOGO_PATH;
},
userFullname() {
if (this.user.first_name || this.user.last_name) {
return `${this.user.first_name} ${this.user.last_name}`;
}
return null;
},
isAdmin() {
return this.USER_LEVEL_PROJECTS &&
this.USER_LEVEL_PROJECTS[this.project.slug] === 'Administrateur projet'
? true
: false;
},
hasAdminRights() {
return this.user && (this.user.is_administrator || this.user.is_superuser) || this.isAdmin;
},
isSharedProject() {
return this.$route.path.includes('projet-partage');
}
},
created() {
window.addEventListener('mousedown', this.clickOutsideMenu);
},
beforeDestroy() {
window.removeEventListener('mousedown', this.clickOutsideMenu);
},
methods: {
logout() {
this.$store.dispatch('LOGOUT');
},
clickOutsideMenu(e) {
if (e.target.closest && !e.target.closest('#menu-dropdown')) {
this.menuIsOpen = false;
}
},
}
};
</script>
<style lang="less" scoped>
.menu.container .header {
padding-top: 5px !important;
padding-bottom: 5px !important;
&> img {
max-height: 30px;
}
}
.vertical {
flex-direction: column;
justify-content: center;
}
.flex {
display: flex;
}
/* keep above loader */
#menu-dropdown {
z-index: 1001;
}
.expand-center {
width: 100%;
text-align: center;
}
.network-icon {
padding: .5rem !important;
}
.crossed-out {
position: relative;
padding: .2em;
&::before {
content: "";
position: absolute;
top: 45%;
left: -8%;
width: 100%;
border-top: 2px solid #ee2e24;
transform: rotate(45deg);
box-shadow: 0px 0px 0px 1px #373636;
border-radius: 3px;
}
}
@media screen and (max-width: 1110px) {
.abstract{
display: none !important;
}
}
@media screen and (min-width: 726px) {
.mobile {
display: none !important;
}
#app-header {
min-width: 560px;
}
.menu.container {
width: auto !important;
}
.push-right-desktop {
margin-left: auto;
}
}
@media screen and (max-width: 725px) {
.desktop {
display: none !important;
}
div.dropdown-list {
width: 100vw;
}
.menu.container .header {
//width: 70px;
width: 100%;
}
#app-header:not(.index) {
/* make the logo disappear on scroll */
position: sticky;
top: -90px;
height: 80px;
.menu.container {
/* make the logo disappear on scroll */
height: 30px;
position: sticky;
top: 0;
}
}
.menu.container .header > img {
margin: 0;
margin: auto;
max-width: 100%;
}
#menu-dropdown {
width: 100%;
}
#menu-dropdown > span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.menu.container {
position: relative;
}
.disable:hover {
cursor: default !important;
background-color: #373636 !important;
}
/* keep above map controls or buttons */
#app-header {
z-index: 9999;
.menu.container .ui.inverted.icon.menu { /* avoid adding space when messages are displayed */
margin: 0;
display: flex;
flex-wrap: wrap;
}
}
.ui.menu .ui.dropdown .menu {
.item.no-hover:hover {
cursor: auto !important;
background: white !important;
}
}
/* copy style to apply inside nested div */
.ui.menu .ui.dropdown .menu .item {
margin: 0;
text-align: left;
font-size: 1em !important;
padding: 0.78571429em 1.14285714em !important;
background: 0 0 !important;
color: #252525 !important;
text-transform: none !important;
font-weight: 400 !important;
box-shadow: none !important;
transition: none !important;
}
.item.title::before {
background: none !important;
}
.log-item {
height: 100% !important;
}
</style>
\ No newline at end of file
<template>
<div
id="custom-dropdown"
:id="`custom-dropdown${identifier}`"
:class="[
'ui search selection dropdown',
{ 'active visible': isOpen },
......@@ -10,99 +10,186 @@
>
<input
v-if="search"
ref="input"
v-model="input"
class="search"
autocomplete="off"
tabindex="0"
@input="handelInput"
v-on:keyup.enter="select(0)"
v-model="input"
:placeholder="placeholder"
/>
<!-- {{placeholder}} -->
<div class="default text">{{ selected || placeholder }}</div>
:placeholder="placehold"
@input="isOpen = true"
@keyup.enter="select(0)"
@keyup.esc="toggleDropdown(false)"
>
<div
v-if="!input"
class="default text"
>
<div v-if="Array.isArray(selected)">
<span v-if="selected[0]"> {{ selected[0] }} - </span>
<span class="italic">{{ selected[1] }}</span>
</div>
<div v-else>
{{ selectedDisplay }}
</div>
</div>
<i
:class="['dropdown icon', { clear: search && selected }]"
:class="['dropdown icon', { clear: clearable && selected }]"
aria-hidden="true"
@click="clear"
></i>
/>
<div :class="['menu', { 'visible transition': isOpen }]">
<div
v-for="(option, index) in processedOptions || ['No results found.']"
@click="select(index)"
v-for="(option, index) in filteredOptions || ['No results found.']"
:id="option.name && Array.isArray(option.name) ? option.name[0] : option.name || option"
:key="option + index"
:class="[
processedOptions ? 'item' : 'message',
{ 'active selected': option === selected },
filteredOptions ? 'item' : 'message',
{ 'active selected': option.name === selected || option.id === selected },
]"
>{{ option }}</div>
@click="select(index)"
>
<div v-if="option.name && Array.isArray(option.name)">
<span v-if="option.name[0]"> {{ option.name[0] }} - </span>
<span class="italic">{{ option.name[1] }}</span>
</div>
<span v-else-if="option.name">
{{ option.name }}
</span>
<span v-else>
{{ option }}
</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Dropdown",
name: 'Dropdown',
props: ["options", "selected", "disabled", "search", "placeholder"],
computed: {
processedOptions: function () { //* si un objet {name, value}
let options = this.options.map((el) =>
el.constructor == Object ? el.name : el
);
if (this.search && this.input !== "") {
options = this.searchOptions(options);
}
return options.length > 0 ? options : null;
props: {
clearable: {
type: Boolean,
default: null,
},
disabled: {
type: Boolean,
default: null,
},
options: {
type: Array,
default: null,
},
placeholder: {
type: String,
default: null,
},
selected: {
type: [String, Array],
default: null,
},
search: {
type: Boolean,
default: null,
},
},
data() {
return {
isOpen: false,
input: "",
input: '',
identifier: 0,
};
},
computed: {
filteredOptions: function () {
let options = this.options;
if (this.search && this.input !== '') {
options = this.options.filter(this.matchInput);
}
return options.length > 0 ? options : null;
},
placehold() {
return this.input ? '' : this.placeholder;
},
selectedDisplay() { // for project attributes, option are object and selected is an id
if (this.options[0] && this.options[0].name) {
const option = this.options.find(opt => opt.id === this.selected);
if (option) return option.name;
}
return this.selected;
}
},
created() {
const crypto = window.crypto || window.msCrypto;
const array = new Uint32Array(1);
this.identifier = Math.floor(crypto.getRandomValues(array) * 10000);
window.addEventListener('mousedown', this.clickOutsideDropdown);
},
beforeDestroy() {
window.removeEventListener('mousedown', this.clickOutsideDropdown);
},
methods: {
toggleDropdown() {
this.isOpen = !this.isOpen;
toggleDropdown(val) {
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) {
this.clear(); //* clear selected and input
}
this.isOpen = typeof val === 'boolean' ? val : !this.isOpen;
},
select(index) {
// * toggle dropdown is called several time, timeout delay this function to be the last
setTimeout(() => {
this.isOpen = false; // * quick & dirty, car toggle dropdown est rappelé plusieurs fois aileurs, à creuser...
}, 500);
this.$emit("update:selection", this.options[index]);
this.input = "";
this.isOpen = false;
}, 0);
if (this.filteredOptions) {
this.$emit('update:selection', this.filteredOptions[index]);
}
this.input = '';
},
searchOptions(options) {
return options.filter((el) =>
el.toLowerCase().includes(this.input.toLowerCase())
);
matchInput(el) {
let match;
if (el.name && Array.isArray(el.name)) {
match =
el.name[0].toLowerCase().includes(this.input.toLowerCase()) ||
el.name[1].toLowerCase().includes(this.input.toLowerCase());
} else {
match = el.name
? el.name.toLowerCase().includes(this.input.toLowerCase())
: el.toLowerCase().includes(this.input.toLowerCase());
}
return match;
},
clear() {
if (this.search) {
this.input = "";
this.clearSelected();
if (this.clearable && this.selected) {
this.input = '';
this.$emit('update:selection', '');
if (this.isOpen) {
this.toggleDropdown(false);
}
}
},
clearSelected() {
this.$emit("update:selection", "");
},
handelInput() {
this.isOpen = true;
this.clearSelected();
},
clickOutsideDropdown(e) {
if (!e.target.closest("#custom-dropdown")) this.isOpen = false;
if (!e.target.closest(`#custom-dropdown${this.identifier}`) && this.isOpen) {
this.toggleDropdown(false);
}
},
},
created() {
window.addEventListener("mousedown", this.clickOutsideDropdown);
},
beforeDestroy() {
window.removeEventListener("mousedown", this.clickOutsideDropdown);
},
};
</script>
......@@ -110,4 +197,7 @@ export default {
.ui.selection.dropdown .menu > .item {
white-space: nowrap;
}
</style>
\ No newline at end of file
.italic {
font-style: italic;
}
</style>
<template>
<div
:class="['field', { 'disabled': field.disabled }]"
:data-field_type="field.field_type"
data-test="extra-form"
name="extra-form"
>
<div
v-if="field && field.field_type === 'boolean'"
:class="['ui checkbox', { 'disabled': field.disabled }]"
>
<!-- JSON.parse is used in case of receiving a string 'true' or 'false'-->
<input
:id="field.name"
type="checkbox"
:checked="JSON.parse(field.value)"
:name="field.name"
@change="updateStore_extra_form"
>
<label :for="field.name">
{{ displayLabels ? field.label : '' }}
</label>
</div>
<template v-else>
<label
v-if="displayLabels"
:for="field.name"
:class="{ required: field.is_mandatory }"
>
{{ field.label }}
</label>
<input
v-if="field && field.field_type === 'char'"
:id="field.name"
:value="field.value"
type="text"
:name="field.name"
:required="field.is_mandatory"
@blur="updateStore_extra_form"
>
<textarea
v-else-if="field && field.field_type === 'text'"
:value="field.value"
:name="field.name"
:required="field.is_mandatory"
rows="3"
@blur="updateStore_extra_form"
/>
<input
v-else-if="field && field.field_type === 'integer'"
:id="field.name"
:value="field.value"
type="number"
:name="field.name"
:required="field.is_mandatory"
@change="updateStore_extra_form"
>
<input
v-else-if="field && field.field_type === 'decimal'"
:id="field.name"
:value="field.value"
type="number"
step=".01"
:name="field.name"
:required="field.is_mandatory"
@change="updateStore_extra_form"
>
<input
v-else-if="field && field.field_type === 'date'"
:id="field.name"
:value="field.value"
type="date"
:name="field.name"
:required="field.is_mandatory"
@change="updateStore_extra_form"
>
<Dropdown
v-else-if="field && (field.field_type === 'list' || field.field_type === 'notif_group')"
:options="field.field_type === 'notif_group' ? usersGroupsFeatureOptions : field.options"
:selected="selected_extra_form_list"
:selection.sync="selected_extra_form_list"
:required="field.is_mandatory"
:search="true"
:clearable="true"
/>
<div
v-else-if="field && field.field_type === 'multi_choices_list'"
class="checkbox_list"
>
<div
v-for="option in field.options"
:key="option.id || option"
class="ui checkbox"
>
<input
:id="option.id || option"
type="checkbox"
:checked="field.value && field.value.includes(option.id || option)"
:name="option.id || option"
@change="selectMultipleCheckbox"
>
<label :for="option.id || option">
{{ option.name || option }}
</label>
</div>
</div>
<Multiselect
v-else-if="field && field.field_type === 'pre_recorded_list'"
v-model="selectedPrerecordedValue"
:options="selectedPrerecordedListValues[field.options[0]] || []"
:options-limit="10"
:allow-empty="!field.is_mandatory"
track-by="label"
label="label"
:reset-after="false"
select-label=""
selected-label=""
deselect-label=""
:searchable="true"
:placeholder="'Recherchez une valeur de la liste pré-définie ...'"
:show-no-results="true"
:loading="loadingPrerecordedListValues"
:clear-on-select="false"
:preserve-search="false"
@search-change="search"
@select="selectPrerecordedValue"
>
<template slot="clear">
<div
v-if="selectedPrerecordedValue"
class="multiselect__clear"
@click.prevent.stop="clearPrerecordedValue"
>
<i
class="close icon"
aria-hidden="true"
/>
</div>
</template>
<span slot="noResult">
Aucun résultat.
</span>
<span slot="noOptions">
Saisissez les premiers caractères ...
</span>
</Multiselect>
</template>
</div>
</template>
<script>
import { mapState, mapActions, mapMutations, mapGetters } from 'vuex';
import Multiselect from 'vue-multiselect';
import Dropdown from '@/components/Dropdown.vue';
export default {
name: 'ExtraForm',
components: {
Dropdown,
Multiselect
},
props: {
field: {
type: Object,
default: null,
},
useValueOnly: {
type: Boolean,
default: false,
}
},
data() {
return {
error: null,
prerecordedListSearchQuery: null,
loadingPrerecordedListValues: false,
selectedPrerecordedValue: null,
selectedMultipleCheckbox: [],
};
},
computed: {
...mapState('feature-type', [
'selectedPrerecordedListValues'
]),
...mapState('feature', [
'extra_forms',
]),
...mapGetters(['usersGroupsFeatureOptions']),
selected_extra_form_list: {
get() {
if (this.field.field_type === 'notif_group') {
const usersGroup = this.usersGroupsFeatureOptions.find((group) => group.value === this.field.value);
return usersGroup ? usersGroup.name : '';
}
return this.field.value || '';
},
set(newValue) {
//* set the value selected in the dropdown
if (this.useValueOnly) {
this.$emit('update:value', newValue.id || newValue);
} else {
const newExtraForm = this.field;
newExtraForm['value'] = this.field.field_type === 'notif_group' ? newValue.value : newValue;
this.$store.commit('feature/UPDATE_EXTRA_FORM', newExtraForm);
}
},
},
displayLabels() {
return this.$route.name === 'editer-signalement' || this.$route.name === 'ajouter-signalement' || this.$route.name === 'editer-attribut-signalement';
},
},
watch: {
/**
* Watches for changes in the 'field.value' and updates the form state accordingly.
* This watcher handles specific field types, ensuring their values are correctly initialized
* and updated in scenarios like fast edition mode where certain values might not be auto-refreshed.
*
* @param {*} newValue - The new value of the field.
* @param {*} oldValue - The previous value of the field before the change.
*/
'field.value': function(newValue, oldValue) {
// Check if the field object exists.
if (this.field) {
// Handle pre-recorded list fields specifically.
if (this.field.field_type === 'pre_recorded_list') {
// Update the form value if both new and old values are defined and different,
// or if either value is undefined, indicating a change.
if ((newValue && oldValue && (newValue.label !== oldValue.label || newValue !== oldValue))
|| !newValue || !oldValue) {
this.initPrerecordedXform(); // Reinitialize the field to reflect the updated value.
}
} else if (this.field.field_type === 'multi_choices_list') {
// For multi-choice lists, reinitialize the field if the array values have changed.
// This is crucial in fast edition modes to prevent overriding the current value with a stale value.
this.initMultipleCheckboxXform();
}
// Reset any error states for the field.
this.error = null;
}
},
prerecordedListSearchQuery(newValue) {
this.loadingPrerecordedListValues = true;
this.GET_SELECTED_PRERECORDED_LIST_VALUES({
name: this.field.options[0],
pattern: newValue
})
.then(() => {
this.loadingPrerecordedListValues = false;
})
.catch(() => {
this.loadingPrerecordedListValues = false;
});
}
},
created() {
if (this.field) {
if (this.field.field_type === 'pre_recorded_list') {
this.initPrerecordedXform();
} else if (this.field.field_type === 'multi_choices_list') {
this.initMultipleCheckboxXform();
}
}
},
mounted() {
// autoset field to false if is a boolean, since user doesn't need to select it, when false value is expected
if (this.field.field_type === 'boolean' && (this.field.value === undefined || this.field.value === null)) {
this.updateStore_extra_form(false);
}
},
methods: {
...mapActions('feature-type', [
'GET_SELECTED_PRERECORDED_LIST_VALUES'
]),
...mapMutations('feature', [
'UPDATE_EXTRA_FORM',
'SET_EXTRA_FORMS',
]),
initMultipleCheckboxXform() {
this.selectedMultipleCheckbox = typeof this.field.value === 'string' ? this.field.value.split(',') : this.field.value || [];
},
initPrerecordedXform() {
const { options, value } = this.field;
this.loadingPrerecordedListValues = true;
this.GET_SELECTED_PRERECORDED_LIST_VALUES({
name: options[0],
pattern: ''
})
.then(() => {
this.loadingPrerecordedListValues = false;
})
.catch(() => {
this.loadingPrerecordedListValues = false;
});
if (value) {
this.selectedPrerecordedValue = { label: value.label ? value.label : value };
} else {
this.selectedPrerecordedValue = null;
}
},
/**
* Updates the Vuex store or component state with the new value for a form field.
* This function handles different types of form fields including boolean, multi-choice lists, and others.
*
* @param {Event|*} val - The new value or an event object.
*/
updateStore_extra_form(val) {
// Check if the field object is defined.
if (this.field) {
let newValue;
// If the function is triggered by an event from a template input.
if (val && val.target) {
// For boolean fields (like checkboxes), use the 'checked' property.
if (this.field.field_type === 'boolean') {
newValue = val.target.checked;
} else {
// For other input types, use the 'value' property.
newValue = val.target.value;
}
} else if (this.field.field_type === 'multi_choices_list') {
// For multi-choice lists, the value is stored in component state.
newValue = this.selectedMultipleCheckbox;
} else {
// If the function is called directly with a value (not from an event).
newValue = val;
}
// Set the new value for the field.
if (this.useValueOnly) {
// If the component is used to update directly a returned value, emit an event with the new value.
this.$emit('update:value', newValue);
} else {
// Otherwise, update the Vuex store with the new value for the extra form field.
this.UPDATE_EXTRA_FORM({ ...this.field, value: newValue });
}
}
},
checkForm() {
let isValid = true;
if (this.field && this.field.is_mandatory && !this.field.value) {
isValid = false;
this.error = 'Ce champ est obligatoire';
} else {
this.error = null;
}
return isValid;
},
search(text) {
this.prerecordedListSearchQuery = text;
},
selectPrerecordedValue(e) {
this.selectedPrerecordedValue = e;
this.prerecordedListSearchQuery = null;
this.updateStore_extra_form({ target: { value: this.selectedPrerecordedValue.label } });
},
clearPrerecordedValue() {
this.selectedPrerecordedValue = null;
this.prerecordedListSearchQuery = null;
this.updateStore_extra_form({ target: { value: null } });
},
/**
* Handles the selection and deselection of checkboxes in a form.
* This function updates an array to track the selected checkboxes by their names.
* It's typically called on the change event of each checkbox.
*
* @param {Event} e - The event object from the checkbox input.
*/
selectMultipleCheckbox(e) {
// Destructure the 'checked' status and 'name' of the checkbox from the event target.
const { checked, name } = e.target;
// If the checkbox is checked, add its name to the array of selected checkboxes.
// Cloning the array to allow unsaved changes detection (it wasn't working with Array.push)
if (checked) {
this.selectedMultipleCheckbox = [...this.selectedMultipleCheckbox, name];
} else {
// If the checkbox is unchecked, remove its name from the array.
this.selectedMultipleCheckbox = this.selectedMultipleCheckbox.filter((el) => el !== name);
}
// Call a method to update the Vuex store or component state with the latest selection.
this.updateStore_extra_form();
},
},
};
</script>
<style lang="less" scoped>
label.required:after {
content: ' *';
color: rgb(209, 0, 0);
}
.checkbox_list {
display: flex;
flex-direction: column;
.ui.checkbox {
margin: .25rem 0;
font-weight: normal;
}
}
</style>
<style>
.multiselect__placeholder {
position: absolute;
width: calc(100% - 48px);
overflow: hidden;
text-overflow: ellipsis;
}
.multiselect__tags {
position: relative;
}
/* keep font-weight from overide of semantic classes */
.multiselect__placeholder, .multiselect__content, .multiselect__tags {
font-weight: initial !important;
}
/* keep placeholder eigth */
.multiselect .multiselect__placeholder {
margin-bottom: 9px !important;
padding-top: 1px;
}
/* keep placeholder height when opening dropdown without selection */
input.multiselect__input {
padding: 3px 0 0 0 !important;
}
/* keep placeholder height when opening dropdown with already a value selected */
.multiselect__tags .multiselect__single {
padding: 1px 0 0 0 !important;
margin-bottom: 9px;
}
</style>
<template>
<div>
<h2 class="ui header">
Pièces jointes
</h2>
<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
"
alt="Pièce jointe au signalement"
>
</a>
<div class="middle aligned content">
<a
class="header"
target="_blank"
:href="pj.attachment_file"
>{{
pj.title
}}</a>
<div class="description">
{{ pj.info }}
</div>
</div>
</div>
</div>
<em v-if="attachments.length === 0">
Aucune pièce jointe associée au signalement.
</em>
</div>
</template>
<script>
export default {
name: 'FeatureAttachements',
props: {
attachments: {
type: Array,
default: () => {
return [];
}
}
}
};
</script>
<template>
<div>
<h2 class="ui header">
Activité et commentaires
</h2>
<div
id="feed-event"
class="ui feed"
>
<div
v-for="(event, index) in events"
:key="'event' + index"
>
<div
v-if="event.event_type === 'create'"
>
<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"
>
<br>
<a
:href="
DJANGO_BASE_URL +
event.related_comment.attachment.url
"
target="_blank"
>
<i
class="paperclip fitted icon"
aria-hidden="true"
/>
{{ event.related_comment.attachment.title }}
</a>
</div>
</div>
</div>
</div>
</div>
<div
v-else-if="event.event_type === 'update'"
class="event"
>
<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.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"
aria-hidden="true"
/>
<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>
<div
v-if="enableKeyDocNotif"
class="field"
>
<div class="ui checkbox">
<input
id="is_key_document"
v-model="comment_form.attachment_file.isKeyDocument"
class="hidden"
name="is_key_document"
type="checkbox"
>
<label for="is_key_document">Envoyer une notification de publication aux abonnés du projet</label>
</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 compact green icon button"
@click="postComment"
>
<i
class="plus icon"
aria-hidden="true"
/> Poster le commentaire
</button>
</form>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import featureAPI from '@/services/feature-api';
export default {
name: 'FeatureComments',
props: {
events: {
type: Array,
default: () => {
return [];
}
},
enableKeyDocNotif: {
type: Boolean,
default: false,
}
},
data() {
return {
comment_form: {
attachment_file: {
errors: null,
title: '',
file: null,
isKeyDocument: false
},
comment: {
id_for_label: 'add-comment',
html_name: 'add-comment',
errors: '',
value: null,
},
},
};
},
computed: {
...mapState([
'user',
'isOnline',
]),
...mapGetters([
'permissions',
]),
...mapState('feature', [
'currentFeature',
]),
DJANGO_BASE_URL() {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
},
methods: {
validateForm() {
this.comment_form.comment.errors = '';
if (!this.comment_form.comment.value) {
this.comment_form.comment.errors = 'Le commentaire ne peut pas être vide';
return false;
}
return true;
},
postComment() {
if (this.validateForm()) {
featureAPI
.postComment({
featureId: this.currentFeature.feature_id || this.currentFeature.id,
comment: this.comment_form.comment.value,
})
.then((response) => {
if (response && this.comment_form.attachment_file.file) {
featureAPI
.postCommentAttachment({
featureId: this.currentFeature.feature_id || this.currentFeature.id,
file: this.comment_form.attachment_file.file,
fileName: this.comment_form.attachment_file.fileName,
title: this.comment_form.attachment_file.title,
isKeyDocument: this.comment_form.attachment_file.isKeyDocument,
commentId: response.data.id,
})
.then(() => {
// Reset isKeyDocument to default
this.comment_form.attachment_file.isKeyDocument = false;
this.confirmComment();
});
} else {
this.confirmComment();
}
});
}
},
confirmComment() {
this.$store.commit('DISPLAY_MESSAGE', { comment: 'Ajout du commentaire confirmé', level: 'positive' });
this.$emit('fetchEvents'); //* display new comment on the page
this.comment_form.attachment_file.file = null; //* empty all inputs
this.comment_form.attachment_file.fileName = '';
this.comment_form.attachment_file.title = '';
this.comment_form.comment.value = null;
},
onFileChange(e) {
// * read image file
const files = e.target.files || e.dataTransfer.files;
const handleFile = (isValid) => {
if (isValid) {
this.comment_form.attachment_file.file = files[0]; //* store the file to post afterwards
let title = files[0].name;
this.comment_form.attachment_file.fileName = title; //* name of the file
const fileExtension = title.substring(title.lastIndexOf('.') + 1);
if ((title.length - fileExtension.length) > 11) {
title = `${title.slice(0, 10)}[...].${fileExtension}`;
}
this.comment_form.attachment_file.title = title; //* title for display
this.comment_form.attachment_file.errors = null;
} else {
this.comment_form.attachment_file.errors =
"Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu.";
}
};
if (files.length) {
//* exception for pdf
if (files[0].type === 'application/pdf') {
handleFile(true);
} else {
this.comment_form.attachment_file.errors = null;
//* check if file is an image and pass callback to handle file
this.validateImgFile(files[0], handleFile);
}
}
},
validateImgFile(files, handleFile) {
const url = window.URL || window.webkitURL;
const image = new Image();
image.onload = function () {
handleFile(true);
URL.revokeObjectURL(image.src);
};
image.onerror = function () {
handleFile(false);
URL.revokeObjectURL(image.src);
};
image.src = url.createObjectURL(files);
},
}
};
</script>
<style lang="less" scoped>
.event {
margin-bottom: 1em;
.content {
display: block;
flex: 1 1 auto;
align-self: stretch;
text-align: left;
word-wrap: break-word;
.summary {
margin: 0;
font-size: 1em;
font-weight: 700;
color: #252525;
.date {
display: inline-block;
float: none;
font-weight: 400;
font-size: .85714286em;
font-style: normal;
margin: 0 1em 0 .5em;
padding: 0;
color: rgba(0,0,0,.4);
}
}
.extra.text {
margin-left: 107px;
margin-top: 0;
}
}
}
</style>
\ No newline at end of file
<template>
<div>
<h1 class="ui header">
<div class="content">
<div class="two-block">
<div
v-if="fastEditionMode && form && canEditFeature"
class="form ui half-block"
>
<input
id="feature_detail_title_input"
:value="form.title"
type="text"
required
maxlength="128"
name="title"
@blur="updateTitle"
>
</div>
<div
v-else
class="ellipsis"
>
{{ currentFeature.properties ?
currentFeature.properties.title : currentFeature.id }}
</div>
<div
id="feature-actions"
class="ui icon compact buttons"
>
<div
v-if="queryparams"
class="fast_browsing"
>
<div>
<div>
<strong>
Tri en cours:
</strong>
<span>
par&nbsp;{{ currentSort }}
</span>
</div>
<div>
<strong>
Filtre en cours:
</strong>
<span>
{{ currentFilters }}
</span>
</div>
</div>
<span
class="feature-count"
>
{{ parseInt($route.query.offset) + 1 }} sur {{ featuresCount }}
</span>
<button
id="previous-feature"
:class="['ui button button-hover-green tiny-margin', { disabled: queryparams.previous < 0 }]"
data-tooltip="Voir le précédent signalement"
data-position="bottom center"
@click="toFeature('previous')"
>
<i
class="angle left fitted icon"
aria-hidden="true"
/>
</button>
<button
id="next-feature"
:class="[
'ui button button-hover-green tiny-margin',
{ disabled: queryparams.next >= featuresCount }
]"
data-tooltip="Voir le prochain signalement"
data-position="bottom center"
@click="toFeature('next')"
>
<i
class="angle right fitted icon"
aria-hidden="true"
/>
</button>
</div>
<div>
<button
v-if="fastEditionMode && canEditFeature"
id="save-fast-edit"
:class="['ui button button-hover-orange tiny-margin', { disabled: false }]"
data-tooltip="Enregistrer les modifications"
data-position="bottom center"
@click="fastEditFeature"
>
<i
class="save fitted icon"
aria-hidden="true"
/>
</button>
<router-link
v-if="permissions && permissions.can_create_feature
&& (featureType && !featureType.geom_type.includes('multi'))"
id="add-feature"
:to="{
name: 'ajouter-signalement',
params: {
slug_type_signal: $route.params.slug_type_signal || featureType ? featureType.slug : '',
},
}"
class="ui button button-hover-green tiny-margin"
data-tooltip="Ajouter un signalement"
data-position="bottom center"
>
<i
class="plus icon"
aria-hidden="true"
/>
</router-link>
<router-link
v-if="slugSignal && canEditFeature"
id="edit-feature"
:to="{
name: 'editer-signalement',
params: {
slug_signal: slugSignal,
slug_type_signal: $route.params.slug_type_signal || featureType ? featureType.slug : '',
},
query: $route.query
}"
class="ui button button-hover-orange tiny-margin"
data-tooltip="Éditer le signalement"
data-position="bottom center"
>
<i
class="inverted grey pencil alternate icon"
aria-hidden="true"
/>
</router-link>
<a
v-if="canDeleteFeature && isOnline"
id="currentFeature-delete"
class="ui button button-hover-red tiny-margin"
data-tooltip="Supprimer le signalement"
data-position="bottom right"
@click="$emit('setIsDeleting')"
>
<i
class="inverted grey trash alternate icon"
aria-hidden="true"
/>
</a>
</div>
</div>
</div>
<!-- <div class="ui hidden divider" /> -->
<div class="sub header prewrap">
<span
v-if="fastEditionMode && canEditFeature && form"
class="form ui half-block"
>
<textarea
:value="form.description.value"
name="description"
rows="5"
@blur="updateDescription"
/>
</span>
<span v-else-if="currentFeature && currentFeature.properties">
{{ currentFeature.properties.description }}
</span>
</div>
</div>
</h1>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
export default {
name: 'FeatureHeader',
props: {
featuresCount : {
type: Number,
default: null,
},
slugSignal: {
type: String,
default: '',
},
featureType: {
type: Object,
default: () => {},
},
fastEditionMode: {
type: Boolean,
default: false,
},
isFeatureCreator: {
type: Boolean,
default: false,
},
canEditFeature: {
type: Boolean,
default: false,
},
canDeleteFeature: {
type: Boolean,
default: false,
},
},
computed: {
...mapState([
'user',
'isOnline',
]),
...mapState('feature', [
'currentFeature',
'form',
]),
...mapState('projects', [
'project'
]),
...mapGetters([
'permissions',
]),
queryparams() {
return this.$route.query.offset >= 0 ? {
previous: parseInt(this.$route.query.offset) - 1,
next: parseInt(this.$route.query.offset) + 1
} : null;
},
currentSort() {
const sort = this.$route.query.ordering;
if (sort) {
if (sort.includes('status')) {
return 'statut';
} else if (sort.includes('feature_type')) {
return 'type de signalement';
} else if (sort.includes('title')) {
return 'nom';
} else if (sort.includes('updated_on')) {
return 'date de modification';
} else if (sort.includes('creator')) {
return 'auteur';
} else if (sort.includes('last_editor')) {
return 'dernier éditeur';
}
}
return 'date de création';
},
currentFilters() {
let filters = [];
if (this.$route.query.feature_type_slug) filters.push('type de signalement');
if (this.$route.query.status__value) filters.push('statut');
if (this.$route.query.title) filters.push('titre');
if (filters.length > 0) {
return `par ${filters.join(', ')}`;
} else {
return 'désactivé';
}
}
},
methods: {
toFeature(direction) {
this.$emit('tofeature', {
name: 'details-signalement-filtre',
params: {
slug_type_signal: this.currentFeature.properties.feature_type.slug,
},
query: {
...this.$route.query,
offset: this.queryparams[direction]
}
});
},
updateTitle(e) {
this.$store.commit('feature/UPDATE_FORM_FIELD', { name: 'title', value: e.target.value });
},
updateDescription(e) {
this.$store.commit('feature/UPDATE_FORM_FIELD', { name: 'description', value: e.target.value });
},
fastEditFeature() {
this.$emit('fastEditFeature');
}
}
};
</script>
<style lang="less">
#feature_detail_title_input {
font-weight: bold;
font-size: 2em;
padding: .25em;
}
.two-block {
display: flex;
justify-content: space-between;
margin-bottom: .5em;
}
#feature-actions {
flex-direction: column;
> div {
line-height: initial;
&:last-of-type {
text-align: right;
}
}
> .fast_browsing {
display: flex;
align-items: center;
margin-bottom: .25rem;
span {
margin-left: .1em;
}
span, div {
font-size: 1rem;
color: #666666;
margin-right: 1rem;
font-weight: normal;
opacity: 1 !important;
}
}
}
@media screen and (max-width: 700px) {
.two-block {
flex-direction: column-reverse;
}
#feature-actions.ui.buttons {
flex-direction: column;
align-items: flex-end;
}
}
</style>
\ No newline at end of file
<template>
<div>
<table
class="ui very basic table"
aria-describedby="Table des données du signalement"
>
<tbody>
<tr v-if="featureType">
<td>
<strong> Type de signalement </strong>
</td>
<td>
<FeatureTypeLink :feature-type="featureType" />
</td>
</tr>
<tr
v-for="field in featureFields"
:key="field.name"
>
<template v-if="!field.isDeactivated">
<td>
<strong :class="{ required: field.is_mandatory }">
{{ field.label }}
</strong>
</td>
<td>
<strong class="ui form">
<span
v-if="fastEditionMode && canEditFeature && extra_forms.length > 0"
:id="field.label"
>
<ExtraForm
ref="extraForm"
:field="field"
/>
</span>
<i
v-else-if="field.field_type === 'boolean'"
:class="[
'icon',
field.value ? 'olive check' : 'grey times',
]"
aria-hidden="true"
/>
<span v-else-if="field.value && field.field_type === 'multi_choices_list'">
{{ field.value.join(', ') }}
</span>
<span v-else-if="field.value && field.field_type === 'notif_group'">
{{ usersGroupLabel(field) }}
</span>
<span v-else>
{{ field.value && field.value.label ? field.value.label : field.value }}
</span>
</strong>
</td>
</template>
</tr>
<tr>
<td>
Auteur
</td>
<td v-if="currentFeature.properties">
{{ currentFeature.properties.display_creator }}
</td>
</tr>
<tr>
<td>
Statut
</td>
<td>
<i
v-if="currentFeature.properties && currentFeature.properties.status"
:class="['icon', statusIcon]"
aria-hidden="true"
/>
<FeatureEditStatusField
v-if="fastEditionMode && canEditFeature && form"
:status="form.status.value.value || form.status.value"
class="inline"
/>
<span v-else>
{{ statusLabel }}
</span>
</td>
</tr>
<tr v-if="project && project.feature_assignement">
<td>
Membre assigné
</td>
<td>
<ProjectMemberSelect
:selected-user-id="assignedMemberId"
:disabled="!fastEditionMode || !canEditFeature"
class="inline"
@update:user="$store.commit('feature/UPDATE_FORM_FIELD', { name: 'assigned_member', value: $event })"
/>
</td>
</tr>
<tr>
<td>
Date de création
</td>
<td v-if="currentFeature.properties && currentFeature.properties.created_on">
{{ currentFeature.properties.created_on | formatDate }}
</td>
</tr>
<tr>
<td>
Date de dernière modification
</td>
<td v-if="currentFeature.properties && currentFeature.properties.updated_on">
{{ currentFeature.properties.updated_on | formatDate }}
</td>
</tr>
</tbody>
</table>
<h3>Liaison entre signalements</h3>
<table
class="ui very basic table"
aria-describedby="Table des signalements lié à ce signalement"
>
<tbody>
<tr
v-for="(link, index) in linked_features"
:key="link.feature_to.title + index"
>
<th
v-if="link.feature_to.feature_type_slug"
scope="row"
>
{{ link.relation_type_display }}
</th>
<td
v-if="link.feature_to.feature_type_slug"
>
<FeatureFetchOffsetRoute
:feature-id="link.feature_to.feature_id"
:properties="{
title: link.feature_to.title,
feature_type: { slug: link.feature_to.feature_type_slug }
}"
/>
({{ link.feature_to.display_creator }} -
{{ link.feature_to.created_on }})
</td>
</tr>
<tr v-if="linked_features.length === 0">
<td>
<em>
Aucune liaison associée au signalement.
</em>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import FeatureTypeLink from '@/components/FeatureType/FeatureTypeLink';
import FeatureEditStatusField from '@/components/Feature/FeatureEditStatusField';
import ExtraForm from '@/components/ExtraForm';
import FeatureFetchOffsetRoute from '@/components/Feature/FeatureFetchOffsetRoute';
import ProjectMemberSelect from '@/components/ProjectMemberSelect';
import { statusChoices, formatStringDate, checkDeactivatedValues } from '@/utils';
export default {
name: 'FeatureTable',
filters: {
formatDate(value) {
return formatStringDate(value);
},
},
components: {
FeatureTypeLink,
FeatureEditStatusField,
ExtraForm,
FeatureFetchOffsetRoute,
ProjectMemberSelect,
},
props: {
featureType: {
type: Object,
default: () => {},
},
fastEditionMode: {
type: Boolean,
default: false,
},
canEditFeature: {
type: Boolean,
default: false,
},
},
computed: {
...mapState('projects', [
'project'
]),
...mapState('feature', [
'currentFeature',
'linked_features',
'form',
'extra_forms',
]),
...mapGetters(['usersGroupsFeatureOptions']),
statusIcon() {
switch (this.currentFeature.properties.status.value || this.currentFeature.properties.status) {
case 'archived':
return 'grey archive';
case 'pending':
return 'teal hourglass outline';
case 'published':
return 'olive check';
case 'draft':
return 'orange pencil alternate';
default:
return '';
}
},
statusLabel() {
if (this.currentFeature.properties) {
if (this.currentFeature.properties && this.currentFeature.properties.status.label) {
return this.currentFeature.properties.status.label;
}
const status = statusChoices.find(
(el) => el.value === this.currentFeature.properties.status
);
return status ? status.name : '';
}
return '';
},
featureData() {
if (this.currentFeature.properties && this.featureType) {
// retrieve value for each feature type custom field within feature data
const extraFieldsWithValue = this.featureType.customfield_set.map((xtraField) => {
return {
...xtraField,
value: this.currentFeature.properties[xtraField.name]
};
});
// filter out fields not meeting condition to be activated
return checkDeactivatedValues(extraFieldsWithValue);
}
return [];
},
featureFields() {
return this.fastEditionMode ? this.extra_forms : this.featureData;
},
assignedMemberId() {
if (this.form && this.form.assigned_member) {
return this.form.assigned_member.value;
}
return this.currentFeature.properties.assigned_member;
},
},
methods: {
usersGroupLabel(field) {
const usersGroup = this.usersGroupsFeatureOptions.find((group) => group.value === field.value);
return usersGroup ? usersGroup.name : field.value;
}
}
};
</script>
<style lang="less" scoped>
td {
strong.required:after {
margin: -0.2em 0em 0em 0.2em;
content: '*';
color: #ee2e24;
}
}
</style>
<template>
<section class="ui teal segment">
<h4>
Pièce jointe
<button
class="ui small compact right floated icon button remove-formset"
type="button"
@click="removeAttachmentForm(form.dataKey)"
>
<i
class="ui times icon"
aria-hidden="true"
/>
</button>
</h4>
<div class="visible-fields">
<div class="two fields">
<div class="required field">
<label :for="form.title.id_for_label">{{ form.title.label }}</label>
<input
:id="form.title.id_for_label"
v-model="form.title.value"
type="text"
required
:maxlength="form.title.field.max_length"
:name="form.title.html_name"
>
<ul
:id="form.title.id_for_error"
class="errorlist"
>
<li
v-for="error in form.title.errors"
:key="error"
>
{{ error }}
</li>
</ul>
</div>
<div class="required field">
<label>Fichier (PDF, PNG, JPEG)</label>
<label
class="ui icon button"
:for="'attachment_file' + attachmentForm.dataKey"
>
<i
class="file icon"
aria-hidden="true"
/>
<span
v-if="form.attachment_file.value"
class="label"
>{{
form.attachment_file.value
}}</span>
<span
v-else
class="label"
>Sélectionner un fichier ... </span>
</label>
<input
:id="'attachment_file' + attachmentForm.dataKey"
type="file"
accept="application/pdf, image/jpeg, image/png"
style="display: none"
:name="form.attachment_file.html_name"
@change="onFileChange"
>
<ul
:id="form.attachment_file.id_for_error"
class="errorlist"
>
<li
v-for="error in form.attachment_file.errors"
:key="error"
>
{{ error }}
</li>
</ul>
</div>
</div>
<div class="field">
<label for="form.info.id_for_label">{{ form.info.label }}</label>
<textarea
v-model="form.info.value"
name="form.info.html_name"
rows="5"
/>
</div>
<div
v-if="enableKeyDocNotif"
class="field"
>
<div class="ui checkbox">
<input
:id="'is_key_document-' + attachmentForm.dataKey"
v-model="form.is_key_document.value"
:name="'is_key_document-' + attachmentForm.dataKey"
class="hidden"
type="checkbox"
@change="updateStore"
>
<label :for="'is_key_document-' + attachmentForm.dataKey">
Envoyer une notification de publication aux abonnés du projet
</label>
</div>
</div>
</div>
</section>
</template>
<script>
export default {
name: 'FeatureAttachmentForm',
props: {
attachmentForm: {
type: Object,
default: null,
},
enableKeyDocNotif: {
type: Boolean,
default: false,
}
},
data() {
return {
fileToImport: null,
form: {
id: {
value: '',
errors: null,
label: 'ID',
},
title: {
errors: [],
id_for_error: `errorlist-title-${this.attachmentForm.dataKey}`,
id_for_label: 'titre',
field: {
max_length: 30, // todo : vérifier dans django
},
html_name: 'titre',
label: 'Titre',
value: '',
},
attachment_file: {
errors: [],
id_for_error: `errorlist-file-${this.attachmentForm.dataKey}`,
html_name: 'titre',
label: 'Titre',
value: null,
},
info: {
value: '',
errors: null,
label: 'Info',
},
is_key_document: {
value: false,
},
},
};
},
watch: {
attachmentForm(newValue) {
this.initForm(newValue);
},
//* utilisation de watcher, car @change aurait un délai
'form.title.value': function (newValue, oldValue) {
if (oldValue !== '') {
if (newValue !== oldValue) {
this.updateStore();
}
}
},
'form.info.value': function (newValue, oldValue) {
if (oldValue !== '') {
if (newValue !== oldValue) {
this.updateStore();
}
}
},
},
mounted() {
this.initForm(this.attachmentForm);
},
methods: {
initForm(attachmentForm) {
for (const el in attachmentForm) {
if (el && this.form[el]) {
if (el === 'attachment_file' && attachmentForm[el]) {
this.form[el].value = attachmentForm[el].split('/').pop(); //* keep only the file name, not the path
} else {
this.form[el].value = attachmentForm[el];
}
}
}
},
removeAttachmentForm() {
this.$store.commit(
'feature/REMOVE_ATTACHMENT_FORM',
this.attachmentForm.dataKey
);
if (this.form.id.value) {
this.$store.commit('feature/ADD_ATTACHMENT_TO_DELETE', this.form.id.value);
}
},
updateStore() {
const data = {
id: this.form.id.value,
dataKey: this.attachmentForm.dataKey,
title: this.form.title.value,
attachment_file: this.form.attachment_file.value,
info: this.form.info.value,
fileToImport: this.fileToImport,
is_key_document: this.form.is_key_document.value
};
this.$store.commit('feature/UPDATE_ATTACHMENT_FORM', data);
},
validateImgFile(files, handleFile) {
const url = window.URL || window.webkitURL;
const image = new Image();
image.onload = function () {
handleFile(true);
URL.revokeObjectURL(image.src);
};
image.onerror = function () {
handleFile(false);
URL.revokeObjectURL(image.src);
};
image.src = url.createObjectURL(files);
},
onFileChange(e) {
// * read image file
const files = e.target.files || e.dataTransfer.files;
const _this = this; //* 'this' is different in onload function
function handleFile(isValid) {
if (isValid) {
_this.fileToImport = files[0]; //* store the file to post later
_this.form.attachment_file.value = files[0].name; //* add name to the form for display, in order to match format return from API
_this.updateStore();
_this.form.attachment_file.errors = [];
} else {
_this.form.attachment_file.errors.push(
"Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu."
);
}
}
if (files.length) {
//* exception for pdf
if (files[0].type === 'application/pdf') {
handleFile(true);
} else {
this.form.attachment_file.errors = [];
//* check if file is an image and pass callback to handle file
this.validateImgFile(files[0], handleFile);
}
}
},
checkForm() {
let isValid = true;
if (this.form.title.value === '') {
this.form.title.errors = ['Veuillez compléter ce champ.'];
document
.getElementById(this.form.title.id_for_error)
.scrollIntoView({ block: 'start', inline: 'nearest' });
isValid = false;
} else if (this.form.attachment_file.value === null) {
this.form.attachment_file.errors = ['Veuillez compléter ce champ.'];
this.form.title.errors = [];
document
.getElementById(this.form.attachment_file.id_for_error)
.scrollIntoView({ block: 'start', inline: 'nearest' });
isValid = false;
} else {
this.form.title.errors = [];
this.form.attachment_file.errors = [];
}
return isValid;
},
},
};
</script>