Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request User Emails for Certain Site Features #181

Merged
merged 11 commits into from
May 7, 2024
143 changes: 143 additions & 0 deletions src/components/common/EmailPrompt.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<template>
<Dialog v-model:visible="visible" :closable="false" :header=title :style="{ width: '25rem' }">
<span class="p-text-secondary block mb-5">{{ dialog }}</span>
<div class="flex align-items-center gap-3 mb-3 p-float-label">
<InputText v-model:model-value="email" id="email" class="flex-auto" />
bencap marked this conversation as resolved.
Show resolved Hide resolved
<label for="email">Email</label>
</div>
<div><span v-if="emailValidationError" class="mave-field-error">{{ emailValidationError }}</span></div>
<div class="flex justify-content-end gap-2">
<Button type="button" label="Now now" severity="secondary" @click="ignoreEmail"></Button>
<Button type="button" label="Add email" @click="saveEmail"></Button>
</div>
</Dialog>
</template>

<script>
import axios from 'axios'
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
import InputText from 'primevue/inputtext'

import useItem from '@/composition/item'
import config from '@/config'
import { ref } from 'vue'

export default {
components: { Button, Dialog, InputText },

props: {
title: {
type: String,
required: false,
default: "Add User Email"
},
dialog: {
type: String,
required: false,
default: "You must add an email address to your account to use this feature. You can do so below, or on the 'Settings' page."
},
isFirstLoginPrompt: {
type: Boolean,
required: true
}
},

setup: () => {
const { item: user, setItemId: setUserId, saveItem: saveUser } = useItem({ itemTypeName: 'me' })
const visible = ref(false);

return {
user,
setUserId,
saveUser,
visible,
}
},

data: function() {
return {
email: null,
emailValidationError: null
}
},

watch: {
user: {
handler: function() {
this.email = this.user?.email

if (this.$props.isFirstLoginPrompt) {
this.visible = this.user.isFirstLogin && this.user.email == null
}
else {
this.visible = this.user.email == null
}
}
}
},

mounted: function() {
this.setUserId('me')
},

methods: {
saveEmail: async function() {
const email = this.email ? this.email.trim() : null

if (!email) {
this.emailValidationError = "Email cannot be empty."
}

if (this.user && email && email != this.user.email) {
let errorResponse = null
try {
await this.saveUser({
item: {
...this.user,
email
}
})
} catch (error) {
errorResponse = error.response
}

if (errorResponse) {
if (typeof errorResponse.data.detail === 'string' || errorResponse.data.detail instanceof String) {
// Handle generic errors that are not surfaced by the API as objects
this.$toast.add({ severity: 'error', summary: `Encountered an error saving email: ${errorResponse.data.detail}` })
}
else {
// There will only be one validation error surfaced for emails.
this.emailValidationError = errorResponse.data.detail[0].msg
}
}
else {
this.emailValidationError = null
this.user.email = email
this.$toast.add({ severity: 'success', summary: `Your email was succesfully updated.`, life: 3000 })
this.visible = false
}
}
},

// Save the user with a null email. Saving the user ensures their `is_first_login` value is set to `False`, which
// makes sure they are not prompted with this component again.
ignoreEmail: async function() {
bencap marked this conversation as resolved.
Show resolved Hide resolved
await axios.put(
`${config.apiBaseUrl}/users/me/has-logged-in`,
{},
{
headers: {
accept: 'application/json'
}
}
)
this.visible = false
}
}
}

</script>

<style scoped src="../../assets/forms.css"></style>
8 changes: 7 additions & 1 deletion src/components/layout/DefaultLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

<div class="mavedb-default-layout mavedb-fill-parent">
<Toolbar />
<EmailPrompt
title="Welcome to MaveDB!"
dialog="We're glad you're here! We require a valid email address to upload data to MaveDB, so that we can get in touch if there are any issues. You may add an email now, or do so at any time on the 'Settings' page."
:isFirstLoginPrompt="true"
/>
<div :class="wrapperClasses">
<div :class="mainClasses">
<slot />
Expand All @@ -14,11 +19,12 @@
<script>

import Toolbar from '@/components/layout/Toolbar'
import EmailPrompt from '@/components/common/EmailPrompt.vue'
import '@fontsource/raleway'
import '@/assets/app.css'

export default {
components: {Toolbar},
components: {Toolbar, EmailPrompt},

props: {
height: {
Expand Down
31 changes: 21 additions & 10 deletions src/components/screens/ExperimentEditor.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<template>
<DefaultLayout>
<EmailPrompt
dialog="You must add an email address to your account to create or edit an experiment. You can do so below, or on the 'Settings' page."
:isFirstLoginPrompt="false"
/>
{{ experimentSetUrn }}
<div class="mave-experiment-editor">
<div class="grid">
Expand Down Expand Up @@ -243,6 +247,7 @@ import TabView from 'primevue/tabview'
import Textarea from 'primevue/textarea'

import DefaultLayout from '@/components/layout/DefaultLayout'
import EmailPrompt from '@/components/common/EmailPrompt'
import useItem from '@/composition/item'
import useItems from '@/composition/items'
import config from '@/config'
Expand All @@ -251,7 +256,7 @@ import useFormatters from '@/composition/formatters'

export default {
name: 'ExperimentEditor',
components: { AutoComplete, Button, Card, Chips, Multiselect, DefaultLayout, FileUpload, InputText, ProgressSpinner, TabPanel, TabView, Textarea },
components: { AutoComplete, Button, Card, Chips, Multiselect, DefaultLayout, EmailPrompt, FileUpload, InputText, ProgressSpinner, TabPanel, TabView, Textarea },

setup: () => {
const publicationIdentifierSuggestions = useItems({itemTypeName: 'publication-identifier-search'})
Expand Down Expand Up @@ -565,17 +570,23 @@ export default {
this.$toast.add({severity:'success', summary: 'The new experiment was saved.', life: 3000})
}
} else if (response.data && response.data.detail) {
const formValidationErrors = {}
for (const error of response.data.detail) {
let path = error.loc
if (path[0] == 'body') {
path = path.slice(1)
if (typeof response.data.detail === 'string' || response.data.detail instanceof String) {
// Handle generic errors that are not surfaced by the API as objects
this.$toast.add({ severity: 'error', summary: `Encountered an error saving experiment: ${response.data.detail}` })
}
else {
const formValidationErrors = {}
for (const error of response.data.detail) {
let path = error.loc
if (path[0] == 'body') {
path = path.slice(1)
}
path = path.join('.')
formValidationErrors[path] = error.msg
}
path = path.join('.')
formValidationErrors[path] = error.msg
this.serverSideValidationErrors = formValidationErrors
this.mergeValidationErrors()
}
this.serverSideValidationErrors = formValidationErrors
this.mergeValidationErrors()
}
},

Expand Down
13 changes: 9 additions & 4 deletions src/components/screens/ScoreSetEditor.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<template>
<EmailPrompt
dialog="You must add an email address to your account to create or edit a score set. You can do so below, or on the 'Settings' page."
:isFirstLoginPrompt="false"
/>
<DefaultLayout>
<div class="mave-score-set-editor">
<div class="grid">
Expand Down Expand Up @@ -325,11 +329,11 @@
<div class="field">
<span class="p-float-label">
<AutoComplete
ref="taxonomyInput"
v-model="taxonomy"
ref="taxonomyInput"
v-model="taxonomy"
dropdown
:id="$scopedId('input-targetSequenceTaxonomy')"
:suggestions="taxonomySuggestionsList"
:suggestions="taxonomySuggestionsList"
field="organismName"
:multiple="false"
:options="taxonomies"
Expand Down Expand Up @@ -608,6 +612,7 @@ import { ref } from 'vue'

import EntityLink from '@/components/common/EntityLink'
import DefaultLayout from '@/components/layout/DefaultLayout'
import EmailPrompt from '@/components/common/EmailPrompt'
import useItem from '@/composition/item'
import useItems from '@/composition/items'
import config from '@/config'
Expand Down Expand Up @@ -641,7 +646,7 @@ function emptyTargetGene() {

export default {
name: 'ScoreSetEditor',
components: { AutoComplete, Button, Card, Chips, Column, DataTable, DefaultLayout, Dropdown, EntityLink, FileUpload, InputNumber, InputText, Message, Multiselect, ProgressSpinner, SelectButton, TabPanel, TabView, Textarea },
components: { AutoComplete, Button, Card, Chips, Column, DataTable, DefaultLayout, Dropdown, EmailPrompt, EntityLink, FileUpload, InputNumber, InputText, Message, Multiselect, ProgressSpinner, SelectButton, TabPanel, TabView, Textarea },

setup: () => {
const editableExperiments = useItems({
Expand Down
41 changes: 34 additions & 7 deletions src/components/screens/SettingsScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
<label :for="$scopedId('input-email')">Email</label>
</span>
</div>
<div>
<span v-if="emailValidationError" class="mave-field-error">{{ emailValidationError }}</span>
</div>
</template>
<template #footer>
<Button :disabled="!user || email == user.email" icon="pi pi-check" label="Save" @click="saveEmail" />
Expand Down Expand Up @@ -139,7 +142,8 @@ export default {

data: function() {
return {
email: null
email: null,
emailValidationError: null
}
},

Expand Down Expand Up @@ -174,15 +178,36 @@ export default {
cancelEmailEditing: function() {
this.email = this.user?.email
},
saveEmail: function() {
saveEmail: async function() {
const email = this.email ? this.email.trim() : null
if (this.user && email && email != this.user.email) {
this.saveUser({
item: {
...this.user,
email
let errorResponse = null
try {
await this.saveUser({
item: {
...this.user,
email
}
})
} catch (error) {
errorResponse = error.response
}

if (errorResponse) {
if (typeof errorResponse.data.detail === 'string' || errorResponse.data.detail instanceof String) {
// Handle generic errors that are not surfaced by the API as objects
this.$toast.add({ severity: 'error', summary: `Encountered an error saving email: ${errorResponse.data.detail}` })
}
})
else {
// There will only be one validation error surfaced for emails.
this.emailValidationError = errorResponse.data.detail[0].msg
}
}
else {
this.emailValidationError = null
this.user.email = email
this.$toast.add({ severity: 'success', summary: `Your email was succesfully updated.`, life: 3000 })
}
}
},
setActiveRoles: function(newRoles) {
Expand Down Expand Up @@ -230,6 +255,8 @@ export default {

</script>

<style scoped src="../../assets/forms.css"></style>

<style scoped>
.p-card {
margin-bottom: 1em;
Expand Down